# Documentation Intuition is an open knowledge graph. Its data is made of **atoms**, which identify things, and **triples**, which make claims about them. Every atom and every triple has an identifier derived from its content, so the identifier you compute on your own machine is the identifier the protocol registers onchain. Once an atom or a triple is registered, anyone can deposit into its **vault**. Deposits are public signals of how much people stand behind it. This documentation covers the protocol, the packages you build with, and Intuition Core, the open backend you can run yourself. ## Where to start | If you want to | Read | | -------------------------------------------- | --------------------------------------------------------------------------------------- | | Derive an identifier and check it onchain | [Quickstart](https://learn.intuition.box/get-started/quickstart) | | Understand how the parts fit together | [How Intuition works](https://learn.intuition.box/get-started/how-it-works) | | Learn the data model | [Atoms](https://learn.intuition.box/concepts/atoms) and [Triples](https://learn.intuition.box/concepts/triples) | | See how identifiers are derived | [Identifiers](https://learn.intuition.box/concepts/identifiers) | | Understand vaults, deposits, and fees | [Vaults and signals](https://learn.intuition.box/concepts/vaults-and-signals) | | Stand up your own backend | [Run a node](https://learn.intuition.box/node/run-a-node) | | Rebuild the graph from chain events | [Index the chain](https://learn.intuition.box/node/index-the-chain) | | Read and write the graph from an application | [Packages](https://learn.intuition.box/build/packages) and the [REST API](https://learn.intuition.box/build/rest-api) | | Register atoms and triples onchain | [Publish onchain](https://learn.intuition.box/build/publish-onchain) | | Give an AI agent accurate protocol knowledge | [Agent skills](https://learn.intuition.box/agents/agent-skills) | | Add coverage for a new domain | [Classification plugins](https://learn.intuition.box/extend/classification-plugins) | | See how the system is built | [Architecture](https://learn.intuition.box/reference/architecture) and [Data model](https://learn.intuition.box/reference/data-model) | | Look up a term | [Glossary](https://learn.intuition.box/reference/glossary) | --- # Agent skills A general-purpose language model does not reliably know the details of the Intuition protocol. The Intuition agent skills supply those details as verified reference material that an agent loads when a task needs it. They follow the [Agent Skills specification](https://agentskills.io/specification) and work with Claude Code, Codex, and other compatible agents. ## What the skills prevent Agents that work without the skills tend to make the same mistakes: - They cannot discover the contract ABIs, because general block explorers do not index the Intuition network. - They treat term identifiers as integers. Identifiers are `bytes32` values. - They try to create a single term. Creation is batch-only: `createAtoms` and `createTriples` take arrays. - They omit or miscalculate the value a transaction must carry. ## Available skills | Skill | Purpose | | --------------------- | ------------------------------------------------------------------------------------------ | | `intuition` | The reference for protocol transactions: ABIs, encoding, addresses, and value calculations | | `erc8004-agent-layer` | Plans an ERC-8004 trust-provider integration. See [ERC-8004 agent layer](https://learn.intuition.box/docs/agents/erc-8004) | The skills are published in the [0xintuition/agent-skills](https://github.com/0xintuition/agent-skills) repository. ## Install You need Node.js and a package manager. The examples use `pnpm`. 1. Install every skill: ```bash pnpm dlx skills add 0xintuition/agent-skills ``` To install one skill, name it: ```bash pnpm dlx skills add 0xintuition/agent-skills --skill intuition ``` 2. Start a new agent session. The installed skills are available in it. Invoke a skill by name, for example `/intuition`. Agents that support skills also invoke one on their own when a task matches it. ## What the `intuition` skill produces The `intuition` skill reads current costs from the contract, builds the calldata and value, and simulates the transaction. It then outputs one unsigned transaction as JSON: ```json { "to": "0x", "data": "0x", "value": "", "chainId": "" } ``` The skill does not hold keys and does not send transactions. Your application, wallet, or backend remains responsible for: - choosing the signer, - presenting the transaction for approval, - broadcasting it, and - handling the receipt. This boundary is deliberate. An agent can prepare a correct transaction without ever being in a position to authorize one. ### Approval policy for unattended agents An agent that runs unattended reads its approval policy from `./.intuition/autonomous-policy.json`, or from the path in `INTUITION_POLICY_PATH`. Without a policy file, the skill uses `manual-review` mode. When the policy requires review, the skill outputs an approval request with `"status": "approval_required"` instead of an executable transaction. Enforce the policy in your signer as well. The skill does not include signing middleware. ## Pin a version The repository's `main` branch is an integration branch, not a stable channel. For production agent workflows, install from a release tag or a commit, and upgrade deliberately. Release tags are named after the skill and its version, in the form `-v`. The repository's `CHANGELOG.md` lists the changes in each release. ## Related - [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain) describes the transactions the skill prepares. - [ERC-8004 agent layer](https://learn.intuition.box/docs/agents/erc-8004) uses the second skill. - [Machine-readable documentation](https://learn.intuition.box/docs/agents/machine-readable-docs) gives an agent this documentation in one file. --- # ERC-8004 agent layer [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) defines onchain registries for autonomous agents: an identity registry, a reputation registry, and a validation registry. The reputation registry records feedback but leaves its aggregation to other systems. Intuition is a place where that aggregation can happen in the open. ## Why a knowledge graph Submitting ERC-8004 feedback puts nothing at stake, so a count of ratings carries little information. Feedback is also held in a separate registry on each chain. In Intuition, an assessment of an agent is a claim like any other. It has a vault, so its credibility is expressed by who backs it and by how much. And because agents, providers, and assessments are terms in one graph, a consumer can ask one system about an agent's standing across providers and chains. ## How an agent is represented An agent is an atom. Three triples connect it to its ERC-8004 registration: | Triple | Purpose | | ------------------------------------- | ------------------------------------------------------------------- | | `(agent, same as, ERC-8004 identity)` | Links the atom to the agent's registration in the identity registry | | `(agent, has type, AIAgent)` | Classifies the atom as an agent | | `(agent, implement, ERC-8004)` | Records that the agent implements the standard | The object of the `same as` triple is an atom whose name is the agent's CAIP-style identifier: ```text eip155:/erc721:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432/ ``` - `` is the EVM chain where the agent is registered, such as `8453` for Base. - `` is the agent's token ID in the ERC-8004 identity registry. The `same as` triple is how you find an agent. Before you create an agent atom, look for an existing `same as` triple whose object is the agent's identifier, and reuse its subject. Intuition references the canonical registration. It does not duplicate or replace it. Further triples can describe an agent: the chain it is available on, its owner, the protocols it uses (MCP, A2A, and OASF), x402 support, trust models, and OASF skills and domains. Their objects come from a shared vocabulary of terms with fixed identifiers, so that every provider describes agents the same way. ## How a trust provider publishes A trust provider is an organization that assesses agents. It publishes each assessment with four triples, the **trust pattern**: ```text (agent, has trust provider, provider) (agent, has trust assessment, assessment source) (assessment source, provided by, provider) (assessment source, has type, Trust Assessment Source) ``` | Triple | Role | | --------------------------------------------- | ---------------------------------------------------------------------------------------------- | | `(agent, has trust provider, provider)` | Records that the provider assesses the agent. Consumers signal which providers they trust here | | `(agent, has trust assessment, source)` | Leads from the agent to the provider's assessment source | | `(source, provided by, provider)` | Leads from the assessment source back to the provider | | `(source, has type, Trust Assessment Source)` | Makes every assessment source findable without knowing the providers | The provider and the assessment source are atoms that the provider creates. The assessment source carries a stable public HTTPS URL, where the provider serves its current assessment. The pattern separates what is stable from what changes. The triples are created once. The provider revises its assessment by updating the document behind the URL, without creating new triples. Deposits on the triples stay where they are, and consumers still discover every assessment through the graph. > [!NOTE] > `has trust provider` records that a provider assesses an agent. It is not > an endorsement of the agent. ## Plan an integration with the agent skill The `erc8004-agent-layer` skill produces a deterministic plan for a provider integration: the canonical agent identity, its classification, the provider link, and the assessment source. It checks the live graph for an existing agent atom and uses only the shared vocabulary's fixed identifiers. ### Prerequisites - The Intuition network you are targeting: mainnet or testnet. - The agent's ERC-8004 chain ID and token ID. - For the provider and for the assessment source, either an existing atom identifier or the metadata for a new atom: a name, a description, an image, and a URL. - The assessment source's public HTTPS URL. ### Steps 1. Install the skill: ```bash pnpm dlx skills add 0xintuition/agent-skills --skill erc8004-agent-layer ``` 2. Ask your agent for a plan, for example: ```text Use the erc8004-agent-layer skill. Prepare the canonical testnet semantic plan that connects ERC-8004 agent 8453:1380 to my provider and assessment resolver. Return the plan and stop before protocol preparation. ``` Here `8453:1380` is the agent's chain ID and token ID. 3. Review the plan. The skill saves it to a file and stops. A complete plan has the status `semantic_plan_ready`. 4. To continue, send a new message that asks the agent to prepare the plan's transactions. The [`intuition` skill](https://learn.intuition.box/docs/agents/agent-skills) then prepares and simulates them. Neither skill signs or broadcasts transactions, and the planning turn never pins metadata or prepares transactions. Each later stage needs its own explicit approval in a new message: a transaction preview, pinning the new atoms' metadata, testnet execution, a mainnet preview, and mainnet execution. An ambiguous reply such as "go ahead" authorizes at most the next preview, never pinning, signing, or broadcasting. Creating new provider or assessment-source atoms means pinning their metadata to IPFS, which requires a pinning credential. Keep it in your application's runtime configuration. The skill never asks for the key and never places it in a prompt or in its output. ## Related - [Agent skills](https://learn.intuition.box/docs/agents/agent-skills) - [Predicates](https://learn.intuition.box/docs/concepts/predicates) - [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals) --- # Machine-readable documentation You can give an agent or a language model this documentation as plain text. It is published in two files that follow the [llms.txt](https://llmstxt.org/) convention. | File | Contents | | -------------------------------------------- | --------------------------------------------------------------------- | | [`/docs/llms.txt`](https://learn.intuition.box/docs/llms.txt) | An index: one line per page, with its title, address, and description | | [`/docs/llms-full.txt`](https://learn.intuition.box/docs/llms-full.txt) | Every page in full, as Markdown, in one file | The index is also served at `/llms.txt`, the site's root, where agents look for it first. Both files are generated from the same source as the pages you are reading, so they always match the site. ## Give the documentation to an agent To give an agent the whole documentation, supply `llms-full.txt`. It is plain Markdown with no navigation or styling, and a comment before each page records the page's address. To let an agent choose what to read, supply `llms.txt`. The agent can then fetch only the pages it needs. ## Copy a single page Each page has a **Copy page** control that copies the page as Markdown, ready to paste into a conversation with a model. ## Combine with agent skills The two serve different purposes: - This documentation explains the system: concepts, guides, and reference. - The [agent skills](https://learn.intuition.box/docs/agents/agent-skills) give an agent verified protocol details for producing transactions. An agent that writes code against Intuition benefits from both. --- # Atom services Atom services exposes the classification and enrichment engines of Intuition Core over HTTP. It has no database: send an input and receive what the pipeline knows about it. It listens on port 4010 by default. Use it when you want classification or metadata for an input without creating an atom, for example to preview a URL before a user confirms it. ## Authentication The service is open by default. When the operator sets `ATOM_SERVICES_AUTH_TOKEN`, every endpoint except `/health`, `/ready`, and `/metrics` requires that token: ```http Authorization: Bearer ``` `` is the value of `ATOM_SERVICES_AUTH_TOKEN`. Requests are also rate limited, by default to 120 a minute. Over the limit, the service returns `429`. ## Classify `POST /v1/classify` identifies what an input refers to. The request member is `input`, a string of up to 10,000 characters. ```bash curl -X POST http://localhost:4010/v1/classify \ -H 'Content-Type: application/json' \ -d '{"input":"https://github.com/vercel/next.js"}' ``` ```json { "ok": true, "status": "complete", "message": "Resolved by github-resolver with 1 candidate atom.", "classification": { "type": "url", "domain": "github", "subtype": "repo", "confidence": 0.97 } } ``` The example is shortened. `status` is `complete`, `partial`, or `placeholder`, and `classification` gives the input's type, its domain and subtype, and a confidence between 0 and 1. ## Process `POST /v1/process` classifies and enriches in one request. The request member is `rawInput`, a string of up to 10,000 characters. ```bash curl -X POST http://localhost:4010/v1/process \ -H 'Content-Type: application/json' \ -d '{"rawInput":"https://en.wikipedia.org/wiki/Ethereum"}' ``` The response has these main members: | Member | Meaning | | ---------------- | ------------------------------------------------------------------------------- | | `status` | `success`, `partial`, or `failed` | | `classification` | The classification, in the same form as `POST /v1/classify` returns | | `enrichment` | The enrichment run: its `artifacts`, `errors`, and `skipped` plugins, or `null` | | `timings` | How long each phase took, in milliseconds | For this URL, with no provider keys configured, the artifacts include OpenGraph data, JSON-LD, icons, the Wikipedia extract, and the Wikidata entity. A plugin that does not apply to the input is reported as skipped. A source that fails is reported as an error, and the request still succeeds with the other artifacts. To choose which plugins run, add an `enrichment` member with a `preset`: `default`, `company`, `music`, `crypto`, `academic`, or `custom`. ## Process in batches `POST /v1/process/batch` accepts several inputs as one job and returns at once, with status `202`: ```bash curl -X POST http://localhost:4010/v1/process/batch \ -H 'Content-Type: application/json' \ -d '{"jobs":[{"rawInput":"https://github.com/oven-sh/bun"},{"rawInput":"https://en.wikipedia.org/wiki/Ethereum"}]}' ``` ```json { "jobId": "...", "status": "accepted", "submittedAt": "...", "total": 2 } ``` Each entry in `jobs` takes the same members as `POST /v1/process`. A batch holds up to 25 jobs by default. Poll `GET /v1/process/batch/:jobId` with the returned `jobId` until its `status` is `complete`, `partial`, or `failed`. The service keeps finished jobs for one hour by default. ## Enrich `POST /v1/enrich` runs enrichment only, on an input that is already classified. Its request member is `input`, a classified atom with its `atomType`, `jsonLd`, and `source`, rather than raw text. For raw text, use `POST /v1/process`. ## Errors An error response has this form: ```json { "ok": false, "error": { "code": "VALIDATION_ERROR", "message": "Request payload validation failed.", "requestId": "..." } } ``` | Status | Code | Meaning | | ------ | ---------------------- | ------------------------------------------------------ | | 400 | `INVALID_JSON` | The request body is not valid JSON | | 400 | `VALIDATION_ERROR` | The body does not match the schema. `details` explains | | 400 | `BATCH_LIMIT_EXCEEDED` | The batch has more jobs than the service allows | | 401 | `UNAUTHORIZED` | The token is missing or wrong | | 404 | `BATCH_JOB_NOT_FOUND` | No batch job has this identifier | | 429 | `RATE_LIMITED` | The rate limit is exceeded. See `retry-after` | | 500 | `INTERNAL_ERROR` | An unexpected service error | ## Health and metrics | Endpoint | Purpose | | -------------- | -------------------------------------------- | | `GET /health` | Liveness | | `GET /ready` | Readiness, with the state of each dependency | | `GET /metrics` | Prometheus metrics | ## Related - [Classification and enrichment](https://learn.intuition.box/docs/concepts/classification-and-enrichment) - [Enrichment providers](https://learn.intuition.box/docs/extend/enrichment-providers) --- # Deposit and redeem A deposit buys shares in a term's vault. It is a signal: support for an atom, agreement with a triple, or disagreement with a triple, made through its counter-triple. Redeeming returns shares to the vault in exchange for TRUST. See [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals) for the model and its fees. ## Before you begin You need: - the `config` object with a wallet client, as set up in [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain#set-up), - an account that holds TRUST, or testnet TRUST, and - the identifier of a registered term. To register one, see [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain). ## Choose a curve Every deposit and redemption names a bonding curve, and each term has one vault per curve. Use the protocol's default curve unless your application has a reason to choose another: ```ts import { multiVaultGetBondingCurveConfig } from '@0xintuition/protocol'; const { defaultCurveId } = await multiVaultGetBondingCurveConfig(config); ``` A term's vault on the default curve opens when the term is created. A vault on another curve opens with the first deposit into it. ## Deposit `deposit` takes the receiver of the shares, the term, the curve, and the minimum number of shares you accept. The amount to deposit is the transaction's value. 1. Preview the deposit to see how many shares it buys after fees: ```ts import { multiVaultPreviewDeposit } from '@0xintuition/protocol'; import { parseEther } from 'viem'; const amount = parseEther('1'); const [expectedShares] = await multiVaultPreviewDeposit(config, { args: [termId, defaultCurveId, amount], }); ``` `termId` is the identifier of the atom, triple, or counter-triple you deposit into. `parseEther('1')` is 1 TRUST, in wei. 2. Set the minimum you accept. The price can move between the preview and the transaction. This example tolerates a 1% change: ```ts const minShares = (expectedShares * 99n) / 100n; ``` 3. Send the deposit: ```ts import { multiVaultDeposit } from '@0xintuition/protocol'; const hash = await multiVaultDeposit(config, { args: [account.address, termId, defaultCurveId, minShares], value: amount, }); await publicClient.waitForTransactionReceipt({ hash }); ``` `account.address` is your own address, so the shares are yours. After the transaction is mined, `multiVaultGetShares` returns your new balance: ```ts import { multiVaultGetShares } from '@0xintuition/protocol'; const shares = await multiVaultGetShares(config, { args: [account.address, termId, defaultCurveId], }); ``` The amount must be at least the contract's minimum deposit, `minDeposit`, which you can read with `multiVaultGetGeneralConfig`. If the deposit would buy fewer shares than `minShares`, the transaction reverts and you keep your TRUST. ### Signal disagreement with a triple To disagree with a triple, deposit into its counter-triple: ```ts import { calculateCounterTripleId } from '@0xintuition/ids'; const counterTripleId = calculateCounterTripleId(tripleId); ``` Then deposit into `counterTripleId` as above. An account can hold shares on only one side of a triple per curve. To change sides, redeem your shares on the other side first. ## Redeem `redeem` takes the receiver of the TRUST, the term, the curve, the number of shares, and the minimum amount you accept. 1. Read how many shares you can redeem: ```ts import { multiVaultMaxRedeem } from '@0xintuition/protocol'; const sharesToRedeem = await multiVaultMaxRedeem(config, { args: [account.address, termId, defaultCurveId], }); ``` To redeem part of a position, use a smaller number. 2. Preview the redemption to see what it pays after fees: ```ts import { multiVaultPreviewRedeem } from '@0xintuition/protocol'; const [expectedAssets] = await multiVaultPreviewRedeem(config, { args: [termId, defaultCurveId, sharesToRedeem], }); const minAssets = (expectedAssets * 99n) / 100n; ``` 3. Send the redemption: ```ts import { multiVaultRedeem } from '@0xintuition/protocol'; const hash = await multiVaultRedeem(config, { args: [account.address, termId, defaultCurveId, sharesToRedeem, minAssets], }); await publicClient.waitForTransactionReceipt({ hash }); ``` The TRUST is sent to the receiver, here your own address. ## Deposit or redeem in batches `multiVaultDepositBatch` and `multiVaultRedeemBatch` act on several vaults in one transaction. They take one receiver and parallel arrays, one entry per vault: ```ts import { multiVaultDepositBatch } from '@0xintuition/protocol'; const amounts = [parseEther('0.5'), parseEther('0.5')]; const hash = await multiVaultDepositBatch(config, { args: [ account.address, [atomId, tripleId], [defaultCurveId, defaultCurveId], amounts, [0n, 0n], ], value: amounts[0] + amounts[1], }); ``` The transaction's value must equal the sum of the amounts. The last array holds the minimum shares for each vault. `0n` accepts any result, so set real minimums from a preview in production. `multiVaultRedeemBatch` takes the receiver, the terms, the curves, the shares, and the minimum amounts. ## Read the result of a transaction The `Deposited` and `Redeemed` events report the exact outcome. The `@0xintuition/protocol` event parsers wait for the receipt and decode them: ```ts import { eventParseDeposited } from '@0xintuition/protocol'; const [event] = await eventParseDeposited(publicClient, hash); event.args.shares; // Shares received event.args.assetsAfterFees; // TRUST added to the vault, after fees ``` `eventParseRedeemed` does the same for redemptions. ## Deposit on behalf of another account By default, only the receiver can deposit for itself or redeem its own shares. An account can allow another address to do either, for example a [fee proxy](https://learn.intuition.box/docs/build/fee-proxy): ```ts import { multiVaultApprove } from '@0xintuition/protocol'; const hash = await multiVaultApprove(config, { args: [operatorAddress, 1], }); ``` `operatorAddress` is the address you approve. The second argument is the approval type: | Value | Allows the address to | | ----- | --------------------------------- | | `0` | Nothing. Revokes an approval | | `1` | Deposit on your behalf | | `2` | Redeem on your behalf | | `3` | Deposit and redeem on your behalf | ## Common errors | Error | Cause | | --------------------------------------- | --------------------------------------------------------------- | | `MultiVault_DepositBelowMinimumDeposit` | The amount is below `minDeposit` | | `MultiVault_SlippageExceeded` | The result is below `minShares` or `minAssets` | | `MultiVault_HasCounterStake` | You hold shares on the other side of this triple, on this curve | | `MultiVault_InsufficientSharesInVault` | You asked to redeem more shares than you can | | `MultiVault_SenderNotApproved` | The sender is not approved to deposit for the receiver | | `MultiVault_RedeemerNotApproved` | The sender is not approved to redeem for the receiver | | `MultiVaultCore_TermDoesNotExist` | The term is not registered | ## Related - [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals) - [Read the chain](https://learn.intuition.box/docs/build/read-the-chain) - [Fee proxy](https://learn.intuition.box/docs/build/fee-proxy) --- # Fee proxy A fee proxy is a contract that sits between your application and `MultiVault`. Users deposit through it. It takes your fee, forwards the rest of each deposit to `MultiVault`, and the shares go directly to the user. Intuition provides a Hardhat template for this contract, the Intuition Fee Proxy template. This page describes how the template behaves and how to deploy it. ## Fees The proxy charges two fees on each deposit, on top of the amount the user deposits: | Fee | Template default | Applies to | | -------------- | ---------------- | -------------------- | | Fixed fee | 0.1 TRUST | Each deposit | | Percentage fee | 5% | The deposited amount | Fees apply to `deposit`, `depositBatch`, and the deposits made as part of `createAtoms` and `createTriples`. The cost of creating a term is passed through to `MultiVault` without a fee. ```text fee = (fixedFee × numberOfDeposits) + (totalDeposited × percentageFee / 10000) ``` For `createAtoms` and `createTriples`, `numberOfDeposits` counts only the non-zero amounts. For `depositBatch`, it counts every entry. | Action | Fee | User sends | | ------------------------------------------- | --------------------------- | ------------ | | One deposit of 10 TRUST | 0.1 + 0.5 = 0.6 TRUST | 10.6 TRUST | | One deposit of 0.05 TRUST | 0.1 + 0.0025 = 0.1025 TRUST | 0.1525 TRUST | | Three deposits of 0.01, 0.05, and 0.1 TRUST | 0.3 + 0.008 = 0.308 TRUST | 0.468 TRUST | `MultiVault` then applies its own fees to the forwarded deposit. See [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals#fees). ## Call the proxy The proxy's functions take the user's address as `receiver`, the account that receives the shares. | Function | Value to send | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `deposit(receiver, termId, curveId, minShares)` | The deposit plus its fee: `getTotalDepositCost(amount)` | | `depositBatch(receiver, termIds, curveIds, assets, minShares)` | The sum of `assets` plus the fee | | `createAtoms(receiver, data, assets, curveId)` | The creation cost, the deposits, and the fee: `getTotalCreationCost(...)` | | `createTriples(receiver, subjectIds, predicateIds, objectIds, assets, curveId)` | The creation cost, the deposits, and the fee: `getTotalCreationCost(...)` | `deposit` and `depositBatch` take the same arguments as on `MultiVault`. `createAtoms` and `createTriples` differ: each amount in `assets` is the deposit on top of the creation cost, not the total, and `curveId` names the curve those deposits go to. `getTotalDepositCost(depositAmount)` returns what to send for one deposit. `getTotalCreationCost(depositCount, totalDeposit, multiVaultCost)` returns what to send for a creation, where `multiVaultCost` is the creation cost times the number of terms, plus the total deposit. `calculateDepositFee` returns the fee alone. > [!NOTE] > Terms created through the proxy record the proxy, not the user, as their > creator onchain. ## User approval Because the proxy deposits on the user's behalf, each user must approve it on `MultiVault` once, with the deposit approval type: ```ts import { multiVaultApprove } from '@0xintuition/protocol'; const hash = await multiVaultApprove(config, { args: [proxyAddress, 1], }); ``` `config` is a configuration with the user's wallet client, as in [Deposit and redeem](https://learn.intuition.box/docs/build/deposit-and-redeem#deposit-on-behalf-of-another-account). `proxyAddress` is your deployed proxy, and `1` is the deposit approval. ## Configure The template reads its configuration from environment variables when it deploys. Copy `.env.example` to `.env` and set: | Variable | Meaning | | -------------------- | ---------------------------------------------------------------------------------------------- | | `PRIVATE_KEY` | The key of the deploying account, which pays for the deployment | | `FEE_RECIPIENT` | The address that receives the fees. It must be an address you control on the Intuition network | | `ADMIN_1`, `ADMIN_2` | Addresses permitted to change the fees and settings. `ADMIN_2` is optional | | `DEPOSIT_FEE` | The fixed fee, in TRUST | | `DEPOSIT_PERCENTAGE` | The percentage fee, in basis points. `500` is 5% | After deployment, an administrator can change the fees with `setDepositFixedFee` and `setDepositPercentageFee`, change the recipient with `setFeeRecipient`, and add or remove administrators with `setWhitelistedAdmin`. ## Deploy 1. Check the `MultiVault` addresses in `scripts/deploy.ts`. They must match the addresses in [Networks and contracts](https://learn.intuition.box/docs/build/networks-and-contracts). Correct them if they differ. 2. Install the dependencies, compile the contract, and run the test suite: ```bash pnpm install pnpm run compile pnpm test ``` 3. Deploy to the testnet, and confirm the behavior with a small deposit: ```bash pnpm run deploy:testnet ``` 4. Deploy to mainnet: ```bash pnpm run deploy:mainnet ``` The script prints the proxy's address and verifies the contract on the explorer. The proxy is not upgradeable. To change its code, deploy a new proxy. ## Considerations - State your fee clearly in your interface. Users see the total they send, and the difference from the amount deposited should not surprise them. - Read through `MultiVault` directly. The proxy passes some reads through, but its `calculateAtomId` does not apply the protocol's derivation and returns a different identifier. Use `@0xintuition/ids` instead. - A proxy holds administrative power over its fees. Protect the administrator keys accordingly. ## Related - [Deposit and redeem](https://learn.intuition.box/docs/build/deposit-and-redeem) - [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain) - [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals) --- # Networks and contracts This page lists the Intuition networks and the addresses of the protocol contracts on each. It is written from `@0xintuition/deployments`. In code, import the values from that package instead of copying them: ```ts import { getContractAddressFromChainId, getMultiVaultAddressFromChainId, intuitionMainnet, intuitionTestnet, } from '@0xintuition/deployments'; const multiVault = getMultiVaultAddressFromChainId(intuitionMainnet.id); const registry = getContractAddressFromChainId( 'BondingCurveRegistry', intuitionMainnet.id, ); ``` `intuitionMainnet` and `intuitionTestnet` are viem chain definitions. Pass them to `createPublicClient` and `createWalletClient`, or to wagmi. ## Networks | | Mainnet | Testnet | | --------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Name | Intuition | Intuition Testnet | | Chain ID | `1155` | `13579` | | Native currency | TRUST, 18 decimals | tTRUST, 18 decimals | | RPC (HTTP) | `https://rpc.intuition.systems/http` | `https://testnet.rpc.intuition.systems/http` | | RPC (WebSocket) | `wss://rpc.intuition.systems/ws` | `wss://testnet.rpc.intuition.systems/ws` | | Explorer | [explorer.intuition.systems](https://explorer.intuition.systems/) | [testnet.explorer.intuition.systems](https://testnet.explorer.intuition.systems/) | The RPC endpoints are public and need no key. Testnet TRUST is available from the faucet at [testnet.hub.intuition.systems](https://testnet.hub.intuition.systems/). On mainnet, TRUST is also an ERC-20 token on Base, and it moves between Base and Intuition through the bridge at [bridge.intuition.systems](https://bridge.intuition.systems/). `@0xintuition/periphery` has helpers for the bridge contracts. ## Protocol contracts | Contract | Role | Mainnet | Testnet | | ---------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | | `MultiVault` | Registers terms and holds their vaults | `0x6E35cF57A41fA15eA0EaE9C33e751b01A784Fe7e` | `0xeBc49d356B7f64D888130D85CC6D17114a6843ec` | | `WrappedTrust` | Wraps native TRUST as an ERC-20 token | `0x81cFb09cb44f7184Ad934C09F82000701A4bF672` | `0xDE80b6EE63f7D809427CA350e30093F436A0fe35` | | `TrustBonding` | Locks TRUST and distributes rewards | `0x635bBD1367B66E7B16a21D6E5A63C812fFC00617` | `0xfa1bF01055239C674845aAEe5A5416f98f801BE3` | | `BondingCurveRegistry` | Lists the bonding curves by curve identifier | `0xd0E488Fb32130232527eedEB72f8cE2BFC0F9930` | `0xbFE8068d5C5117c57d37464e33c937bC08317F88` | | `Multicall3` | Batches several reads into one request | `0xcA11bde05977b3631167028862bE2a173976CA11` | `0xcA11bde05977b3631167028862bE2a173976CA11` | The TRUST token contract is deployed on Base, at `0x6cd905dF2Ed214b22e0d48FF17CD4200C1C6d8A3`, and on Base Sepolia, at `0xA54b4E6e356b963Ee00d1C947f478d9194a1a210`. `@0xintuition/deployments` also has the addresses of the emissions controllers, the individual bonding curves, and the atom wallet contracts. Look them up by name with `getContractAddressFromChainId`. ## Contract source and ABIs The contracts' Solidity source, ABIs, and bytecode are published as [`@0xintuition/contracts-v2`](https://www.npmjs.com/package/@0xintuition/contracts-v2). For application code, use `@0xintuition/protocol`, which exports the ABIs, such as `MultiVaultAbi`, with typed helpers for their functions. ## Local development A local devnet uses chain ID `31337`. `@0xintuition/deployments` does not include it, because its addresses change with each deployment. The deployer writes them to `devnet/deployments-devnet.json`. See [Local devnet](https://learn.intuition.box/docs/node/local-devnet). To use the devnet from an application, use viem's `anvil` chain and read the `MultiVault` address from that file: ```ts import { createPublicClient, http } from 'viem'; import { anvil } from 'viem/chains'; const publicClient = createPublicClient({ chain: anvil, transport: http() }); const address = '0x...'; // MultiVault, from devnet/deployments-devnet.json ``` ## Related - [Read the chain](https://learn.intuition.box/docs/build/read-the-chain) - [Index the chain](https://learn.intuition.box/docs/node/index-the-chain) --- # Packages The `@0xintuition` packages are TypeScript libraries for building on Intuition. They derive identifiers, describe the networks and their contracts, and wrap the protocol's contract calls. Install only the ones you need. The packages are ESM-only: they have no CommonJS entry points. The packages that work with identifiers or the chain declare [viem](https://viem.sh/) 2 as a peer dependency. ## Install For an application that reads from and writes to the protocol, install these four packages: ```bash pnpm add @0xintuition/protocol@3.0.0 @0xintuition/deployments@alpha @0xintuition/ids@alpha viem ``` | Package | What it gives you | | -------------------------- | ---------------------------------------------------------------------- | | `@0xintuition/protocol` | Contract ABIs, with typed helpers for their functions | | `@0xintuition/deployments` | Chain definitions for viem, and contract addresses for each network | | `@0xintuition/ids` | Identifier derivation for atoms, triples, and counter-triples, offline | | `viem` | The client that reads from the chain and sends transactions | ### Versions and tags Most packages are on the `0.1.0-alpha` line, published under the `alpha` distribution tag. `@0xintuition/protocol` is the exception. Version 3 is published under the `alpha` tag, and the `latest` tag still points at the earlier `2.x` line, which has a different API. Always request version 3 explicitly. Version 3 does not include addresses or chain definitions: import those from `@0xintuition/deployments`. > [!NOTE] > These are alpha releases. Pin exact versions in production, and read the > release notes before you upgrade. ## Set up the clients The pages that read from and write to the chain share this setup: a public client to read, a wallet client to send transactions, and the `MultiVault` contract address. ```ts import { getMultiVaultAddressFromChainId, intuitionTestnet, } from '@0xintuition/deployments'; import { createPublicClient, createWalletClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const publicClient = createPublicClient({ chain: intuitionTestnet, transport: http(), }); const walletClient = createWalletClient({ account, chain: intuitionTestnet, transport: http(), }); const address = getMultiVaultAddressFromChainId(intuitionTestnet.id); ``` - `PRIVATE_KEY` is the private key of an account that holds testnet TRUST. Keep it in an environment variable, never in source code. - `http()` with no argument uses the chain's public RPC endpoint. - Replace `intuitionTestnet` with `intuitionMainnet` to use mainnet. The protocol helpers take a configuration object as their first argument. A read helper needs `address` and `publicClient`. A write helper also needs `walletClient`: ```ts const config = { address, publicClient, walletClient }; ``` ### Use a browser wallet In a web application, the user's wallet signs instead of a private key: ```ts import 'viem/window'; import { intuitionTestnet } from '@0xintuition/deployments'; import { createWalletClient, custom } from 'viem'; const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts', }); const walletClient = createWalletClient({ account, chain: intuitionTestnet, transport: custom(window.ethereum!), }); ``` The `viem/window` import declares the type of `window.ethereum`, which the wallet extension injects. ### Use the React hooks `@0xintuition/react` wraps the same contract calls in hooks for applications built with [wagmi](https://wagmi.sh/) and TanStack Query. Install it with its peer dependencies: ```bash pnpm add @0xintuition/react@alpha @0xintuition/protocol@3.0.0 @0xintuition/deployments@alpha wagmi viem @tanstack/react-query ``` Configure wagmi with the chain definitions from `@0xintuition/deployments`. Then wrap your application in `IntuitionProvider`, inside the wagmi and TanStack Query providers, and choose a network: ```tsx import { IntuitionProvider, useAtomCost } from '@0xintuition/react'; export function App() { return ( ); } function AtomCost() { const { data: atomCost } = useAtomCost(); return

{atomCost?.toString()} wei

; } ``` `network` is `mainnet` or `testnet`. The provider resolves the `MultiVault` address from `@0xintuition/deployments`. ## All packages | Package | Purpose | | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | [`@0xintuition/ids`](https://www.npmjs.com/package/@0xintuition/ids) | Derives identifiers for atoms, triples, counter-triples, and predicates | | [`@0xintuition/predicates`](https://www.npmjs.com/package/@0xintuition/predicates) | The canonical predicate registry, with fixed identifiers | | [`@0xintuition/classifications`](https://www.npmjs.com/package/@0xintuition/classifications) | Classification specifications, and builders that produce consistent atom data | | [`@0xintuition/schema-org`](https://www.npmjs.com/package/@0xintuition/schema-org) | A pinned schema.org vocabulary | | [`@0xintuition/primitives`](https://www.npmjs.com/package/@0xintuition/primitives) | Higher-level builders and validators for atoms and triples | | [`@0xintuition/protocol`](https://www.npmjs.com/package/@0xintuition/protocol) | Contract ABIs, contract helpers, and event parsers | | [`@0xintuition/deployments`](https://www.npmjs.com/package/@0xintuition/deployments) | Chain definitions and contract addresses | | [`@0xintuition/curves`](https://www.npmjs.com/package/@0xintuition/curves) | Bonding curve mathematics, for previews without a network call | | [`@0xintuition/periphery`](https://www.npmjs.com/package/@0xintuition/periphery) | Helpers for the bridge and router contracts | | [`@0xintuition/react`](https://www.npmjs.com/package/@0xintuition/react) | React hooks for protocol reads and writes | ## What the packages do not cover The packages talk to the chain, one term at a time. They do not search, filter, or list the graph, and they do not pin files to IPFS. To search atoms, list the triples around an atom, or list an account's positions, query an Intuition Core node through its [REST API](https://learn.intuition.box/docs/build/rest-api) or [with SQL](https://learn.intuition.box/docs/build/query-with-sql). ## Related - [Quickstart](https://learn.intuition.box/docs/get-started/quickstart) - [Read the chain](https://learn.intuition.box/docs/build/read-the-chain) - [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain) - [Deposit and redeem](https://learn.intuition.box/docs/build/deposit-and-redeem) --- # Publish onchain Publishing registers a term with the protocol. The term keeps its identifier. What changes is that it becomes permanent and public, and gains a vault that anyone can deposit into. ## Before you begin You need: - the packages installed, as described in [Packages](https://learn.intuition.box/docs/build/packages), - an account that holds TRUST, or testnet TRUST from [testnet.hub.intuition.systems](https://testnet.hub.intuition.systems/) when you work on the testnet, and - the data of the atoms you intend to create. Develop against the testnet or a [local devnet](https://learn.intuition.box/docs/node/local-devnet) before you send transactions on mainnet. ## Set up ```ts import { getMultiVaultAddressFromChainId, intuitionTestnet, } from '@0xintuition/deployments'; import { createPublicClient, createWalletClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const publicClient = createPublicClient({ chain: intuitionTestnet, transport: http(), }); const walletClient = createWalletClient({ account, chain: intuitionTestnet, transport: http(), }); const config = { address: getMultiVaultAddressFromChainId(intuitionTestnet.id), publicClient, walletClient, }; ``` `PRIVATE_KEY` is the private key of the account that pays for the transactions. To sign with a browser wallet instead, see [Use a browser wallet](https://learn.intuition.box/docs/build/packages#use-a-browser-wallet). ## Check whether a term exists A term can be registered only once. Creating an atom whose data is already registered reverts with `MultiVault_AtomExists`, and creating an existing triple reverts with `MultiVault_TripleExists`. Check before you send: ```ts import { calculateAtomId } from '@0xintuition/ids'; import { multiVaultIsTermCreated } from '@0xintuition/protocol'; import { toHex } from 'viem'; const data = toHex('https://github.com/oven-sh/bun'); const atomId = calculateAtomId(data); const exists = await multiVaultIsTermCreated(config, { args: [atomId] }); ``` `toHex` encodes the text as UTF-8 bytes, which is the form the contract stores. Derive the identifier from the same bytes you send, so the two always match. ## Create atoms `createAtoms` takes two arrays of the same length: the atom data, as bytes, and one amount per atom. 1. Read the current atom cost: ```ts import { multiVaultGetAtomCost } from '@0xintuition/protocol'; const atomCost = await multiVaultGetAtomCost(config); ``` 2. Send the transaction. Its value must equal the sum of the amounts: ```ts import { multiVaultCreateAtoms } from '@0xintuition/protocol'; const hash = await multiVaultCreateAtoms(config, { args: [[data], [atomCost]], value: atomCost, }); ``` 3. Wait for the transaction to be mined: ```ts const receipt = await publicClient.waitForTransactionReceipt({ hash }); ``` `receipt.status` is `success`, and `multiVaultIsTermCreated` now returns `true` for `atomId`. Each amount must be at least the atom cost. Any amount above the cost is deposited, after fees, into the new atom's vault on your behalf. To create several atoms in one transaction, add more entries to both arrays. The contract also limits the size of a call. Read the limits before you build large batches or long atom data: ```ts import { multiVaultGetGeneralConfig } from '@0xintuition/protocol'; const { atomDataMaxLength } = await multiVaultGetGeneralConfig(config); ``` `atomDataMaxLength` is the longest atom data the contract accepts, in bytes. A call also accepts at most `MAX_BATCH_SIZE` entries, a constant of the `MultiVault` contract. The helper simulates the transaction before sending it, so a call that would revert throws an error before any gas is spent. ### Structured atoms An atom's data can be any bytes, but data that follows a shared structure lets applications agree on the same atom. `@0xintuition/classifications` builds JSON-LD atom data for known kinds of things, with the fields in a fixed order: ```ts import { buildAtomData } from '@0xintuition/classifications'; import { toHex } from 'viem'; const accountData = toHex( buildAtomData('ethereum-account', { address: '0x0000000000000000000000000000000000000001', }), ); ``` Install it with `pnpm add @0xintuition/classifications@alpha`. Create `accountData` with `createAtoms`, as above. ## Create triples `createTriples` takes four arrays of the same length: the subject, predicate, and object identifiers, and one amount per triple. Every term must already be registered. A subject or an object can itself be a triple. This example states that a repository has the tag `javascript runtime`, using the canonical `has tag` predicate. A canonical predicate is an atom like any other, and it may not be registered on the network yet. The example first creates whichever of the three atoms are missing, the predicate included. ```ts import { calculateAtomId, calculateTripleId } from '@0xintuition/ids'; import { atomData as hasTagData } from '@0xintuition/predicates/hasTag'; import { multiVaultCreateAtoms, multiVaultCreateTriples, multiVaultGetAtomCost, multiVaultGetTripleCost, multiVaultIsTermCreated, } from '@0xintuition/protocol'; import { toHex, type Hex } from 'viem'; const subject = toHex('https://github.com/oven-sh/bun'); const predicate = toHex(hasTagData); const object = toHex('javascript runtime'); // 1. Create the atoms that are not registered yet. const missing: Hex[] = []; for (const atomData of [subject, predicate, object]) { const isCreated = await multiVaultIsTermCreated(config, { args: [calculateAtomId(atomData)], }); if (!isCreated) missing.push(atomData); } if (missing.length > 0) { const atomCost = await multiVaultGetAtomCost(config); const hash = await multiVaultCreateAtoms(config, { args: [missing, missing.map(() => atomCost)], value: atomCost * BigInt(missing.length), }); await publicClient.waitForTransactionReceipt({ hash }); } // 2. Create the triple. const subjectId = calculateAtomId(subject); const predicateId = calculateAtomId(predicate); const objectId = calculateAtomId(object); const tripleCost = await multiVaultGetTripleCost(config); const hash = await multiVaultCreateTriples(config, { args: [[subjectId], [predicateId], [objectId], [tripleCost]], value: tripleCost, }); await publicClient.waitForTransactionReceipt({ hash }); const tripleId = calculateTripleId(subjectId, predicateId, objectId); ``` As with atoms, each amount must be at least the triple cost, and any amount above it is deposited into the triple's vault. Creating a triple also opens the vault of its counter-triple, so others can signal disagreement. An account can hold shares on only one side of a triple, as described in [Deposit and redeem](https://learn.intuition.box/docs/build/deposit-and-redeem#signal-disagreement-with-a-triple). See also [Triples](https://learn.intuition.box/docs/concepts/triples) and [Predicates](https://learn.intuition.box/docs/concepts/predicates). ## Build transactions without sending them Some components sign elsewhere, such as a smart account or a backend service. For these, the create, deposit, and redeem helpers have `Encode` variants that return calldata instead of sending a transaction: ```ts import { multiVaultCreateAtomsEncode } from '@0xintuition/protocol'; const calldata = multiVaultCreateAtomsEncode([data], [atomCost]); ``` Send the calldata to the `MultiVault` address with a value equal to the sum of the amounts. ## Common errors The contract reverts with a named error. viem includes the name in the error it throws. | Error | Cause | | -------------------------------- | ----------------------------------------------------------- | | `MultiVault_AtomExists` | An atom with this data is already registered | | `MultiVault_TripleExists` | This triple is already registered | | `MultiVault_TermDoesNotExist` | A triple refers to a term that is not registered | | `MultiVault_InsufficientBalance` | The transaction value does not equal the sum of the amounts | | `MultiVault_InsufficientAssets` | An amount is below the atom cost or the triple cost | | `MultiVault_ArraysNotSameLength` | The arrays have different lengths | | `MultiVault_AtomDataTooLong` | The atom data exceeds `atomDataMaxLength` | ## Confirm the result After the transaction is mined, `isTermCreated` returns `true` for each new term. A node that indexes the chain records the new terms as it processes the block. See [Index the chain](https://learn.intuition.box/docs/node/index-the-chain). ## Related - [Deposit and redeem](https://learn.intuition.box/docs/build/deposit-and-redeem) - [Read the chain](https://learn.intuition.box/docs/build/read-the-chain) - [Networks and contracts](https://learn.intuition.box/docs/build/networks-and-contracts) - [Agent skills](https://learn.intuition.box/docs/agents/agent-skills), for agents that prepare these transactions --- # Query with SQL The REST API covers lookups and traversal. For analysis, and for vaults and positions, which the REST API does not serve, connect to the node's databases. Both are PostgreSQL. ## Connect With the default Docker Compose stack, the databases listen on local ports 5432 and 5433: ```bash # The knowledge graph psql postgresql://intuition:intuition@localhost:5432/intuition_kg # The chain event store and market read models psql postgresql://intuition:intuition@localhost:5433/intuition_timescale ``` `make explore` prints a guided summary of the knowledge graph: table counts, recent atoms, pipeline status, predicates, and artifacts. > [!NOTE] > The two databases are separate. There are no foreign keys between them. > They are related by identifier: an atom's identifier in the knowledge graph > is its term identifier in the event store. ## Graph queries ### Counts of public records ```sql SELECT 'nodes' AS table_name, count(*)::bigint AS rows FROM kg.nodes WHERE status = 'active' AND visibility = 'public' UNION ALL SELECT 'triples', count(*)::bigint FROM kg.triples WHERE status = 'active' AND visibility = 'public' UNION ALL SELECT 'predicates', count(*)::bigint FROM kg.predicates UNION ALL SELECT 'accounts', count(*)::bigint FROM kg.accounts ORDER BY table_name; ``` ### Recent atoms and their pipeline state ```sql SELECT created_at, id, raw_type, classification_type, parse_status, classification_status, enrichment_status, left(coalesce(nullif(data, ''), data_hex, ''), 120) AS data_preview FROM kg.nodes WHERE status = 'active' AND visibility = 'public' ORDER BY created_at DESC, id DESC LIMIT 25; ``` ### Most used predicates ```sql SELECT t.predicate_type, t.predicate_id, p.slug, p.label, count(*)::bigint AS triple_count FROM kg.triples t LEFT JOIN kg.predicates p ON t.predicate_type = 'node' AND t.predicate_id = p.id WHERE t.status = 'active' AND t.visibility = 'public' GROUP BY t.predicate_type, t.predicate_id, p.slug, p.label ORDER BY triple_count DESC LIMIT 25; ``` ## Indexing and market queries These queries need data from the indexing profile. See [Index the chain](https://learn.intuition.box/docs/node/index-the-chain). ### Projection lag Shows how far each projection is behind the event store. ```sql WITH maxes AS ( SELECT coalesce(max(sequence_number), 0) AS max_sequence_number, coalesce(max(block_number), 0) AS max_block_number FROM public.event_store WHERE is_canonical = true ) SELECT pc.projection_name, pc.sink_name, pc.last_sequence_number, maxes.max_sequence_number, maxes.max_sequence_number - pc.last_sequence_number AS sequence_lag, pc.last_block_number, maxes.max_block_number - pc.last_block_number AS block_lag, pc.last_updated_at FROM public.projection_checkpoints pc CROSS JOIN maxes ORDER BY sequence_lag DESC, block_lag DESC; ``` ### Terms with the largest market capitalization ```sql SELECT ts.term_id, t.term_type, t.creator, left(coalesce(t.atom_data, t.atom_data_hex, ''), 100) AS term_preview, ts.total_assets, ts.total_market_cap, ts.total_holder_count, ts.updated_at FROM public.term_summary ts LEFT JOIN public.term t ON t.term_id = ts.term_id ORDER BY ts.total_market_cap DESC LIMIT 25; ``` ### Positions of an account Lists the vaults an account holds shares in. Replace the address with the account's address. ```sql SELECT p.term_id, p.curve_id, p.shares, p.total_deposits, left(coalesce(t.atom_data, t.atom_data_hex, ''), 80) AS term_preview, p.updated_at FROM public.position p LEFT JOIN public.term t ON t.term_id = p.term_id WHERE lower(p.account_id) = lower('0x0000000000000000000000000000000000000001') AND p.shares > 0 ORDER BY p.shares DESC; ``` `shares` and `total_deposits` are in wei. `term_preview` is empty for a triple, whose terms are in the `subject_id`, `predicate_id`, and `object_id` columns of `term`. ### Vaults of a term Shows the current state of a term's vaults, one row per bonding curve. ```sql SELECT curve_id, total_shares, current_share_price, total_assets, market_cap, holder_count, updated_at FROM public.vault WHERE lower(term_id) = lower('0x49ed8bdcd1954e1de3af8bb5fca0f0377ebe598de5dd82ab5b4a45a82d53a53f'); ``` ## More queries The Intuition Core repository includes a cookbook of queries in `docs/example-queries.md`. It covers the graph, the worker pipeline, artifacts, indexing, market data, checks across the two databases, and API key administration. ## Related - [Data model](https://learn.intuition.box/docs/reference/data-model) - [REST API](https://learn.intuition.box/docs/build/rest-api) --- # Read the chain Reading from the protocol needs only an RPC endpoint. The public Intuition endpoints need no key. Every read on this page is a call to the `MultiVault` contract, made through a helper from `@0xintuition/protocol`. ## Before you begin Install the packages, as described in [Packages](https://learn.intuition.box/docs/build/packages): ```bash pnpm add @0xintuition/protocol@3.0.0 @0xintuition/deployments@alpha @0xintuition/ids@alpha viem ``` ## Set up a client ```ts import { getMultiVaultAddressFromChainId, intuitionMainnet, } from '@0xintuition/deployments'; import { createPublicClient, http } from 'viem'; const publicClient = createPublicClient({ chain: intuitionMainnet, transport: http(), }); const config = { address: getMultiVaultAddressFromChainId(intuitionMainnet.id), publicClient, }; ``` Replace `intuitionMainnet` with `intuitionTestnet` to read the testnet. The examples below use this `config`. ## Check whether terms are registered `isTermCreated` accepts the identifier of an atom, a triple, or a counter-triple. Derive the identifier offline with `@0xintuition/ids`. ```ts import { calculateAtomId } from '@0xintuition/ids'; import { multiVaultIsTermCreated } from '@0xintuition/protocol'; const atomId = calculateAtomId('hello'); const isCreated = await multiVaultIsTermCreated(config, { args: [atomId] }); ``` To check several terms, use a multicall. It sends one request, however many terms you ask about. ```ts import { MultiVaultAbi } from '@0xintuition/protocol'; import type { Hex } from 'viem'; const ids: Hex[] = [calculateAtomId('hello'), calculateAtomId('world')]; const results = await publicClient.multicall({ allowFailure: false, contracts: ids.map((id) => ({ address: config.address, abi: MultiVaultAbi, functionName: 'isTermCreated' as const, args: [id] as const, })), }); // One boolean per identifier, in the order of ids ``` ## Read a term An atom's data is stored onchain as bytes. Decode it to read text atoms. ```ts import { multiVaultGetAtom } from '@0xintuition/protocol'; import { hexToString } from 'viem'; const data = await multiVaultGetAtom(config, { args: [atomId] }); const text = hexToString(data); // 'hello' ``` A triple is stored as the identifiers of its subject, predicate, and object. ```ts import { multiVaultGetTriple } from '@0xintuition/protocol'; const [subjectId, predicateId, objectId] = await multiVaultGetTriple(config, { args: [tripleId], }); ``` `tripleId` is the identifier of a registered triple. Derive it with `calculateTripleId`, and derive its counter-triple with `calculateCounterTripleId`. Both are in `@0xintuition/ids`. To find out what kind of term an identifier names, use `multiVaultIsAtom`, `multiVaultIsTriple`, or `multiVaultIsCounterTriple`. ## Read a vault Each term has one vault per bonding curve. Terms are created on the protocol's default curve. Read its identifier once and reuse it: ```ts import { multiVaultCurrentSharePrice, multiVaultGetBondingCurveConfig, multiVaultGetVault, } from '@0xintuition/protocol'; const { defaultCurveId } = await multiVaultGetBondingCurveConfig(config); const [totalAssets, totalShares] = await multiVaultGetVault(config, { args: [atomId, defaultCurveId], }); const sharePrice = await multiVaultCurrentSharePrice(config, { args: [atomId, defaultCurveId], }); ``` Amounts are in wei, the smallest unit of TRUST. Format them for display with `formatEther` from viem. See [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals) for how vaults and curves work. ## Read an account's position A position is the number of shares an account holds in one vault. ```ts import { multiVaultConvertToAssets, multiVaultGetShares, } from '@0xintuition/protocol'; const shares = await multiVaultGetShares(config, { args: [accountAddress, atomId, defaultCurveId], }); const value = await multiVaultConvertToAssets(config, { args: [atomId, defaultCurveId, shares], }); ``` `accountAddress` is the address of the account. `value` is what the shares are worth at the current share price, before the fees that apply on redemption. To see what a redemption would pay after fees, preview it, as described in [Deposit and redeem](https://learn.intuition.box/docs/build/deposit-and-redeem#redeem). The contract answers for one account and one vault at a time. To list every position an account holds, query a node. See [Query with SQL](https://learn.intuition.box/docs/build/query-with-sql#positions-of-an-account). ## Read costs and fees The cost of creating a term and the vault fee rates are contract configuration and can change. Read them when you need them. ```ts import { multiVaultGetAtomCost, multiVaultGetGeneralConfig, multiVaultGetTripleCost, multiVaultGetVaultFees, } from '@0xintuition/protocol'; const atomCost = await multiVaultGetAtomCost(config); const tripleCost = await multiVaultGetTripleCost(config); const { entryFee, exitFee, protocolFee } = await multiVaultGetVaultFees(config); const { feeDenominator, minDeposit } = await multiVaultGetGeneralConfig(config); ``` Costs are in wei. A fee rate is a fraction of `feeDenominator`: the fee on an amount is `amount × rate / feeDenominator`. `minDeposit` is the smallest deposit the contract accepts. ## Other read helpers `@0xintuition/protocol` wraps the `MultiVault` functions in helpers with the prefix `multiVault`. These are the ones applications use most. | Helper | Returns | | -------------------------------------------------------------- | ----------------------------------------------------------- | | `multiVaultMaxRedeem` | The most shares an account can redeem from a vault | | `multiVaultConvertToShares` | The shares an amount buys at the current price, before fees | | `multiVaultPreviewDeposit`, `multiVaultPreviewRedeem` | The outcome of a deposit or a redemption, after fees | | `multiVaultPreviewAtomCreate`, `multiVaultPreviewTripleCreate` | The outcome of a creation with an initial deposit | | `multiVaultGetCounterIdFromTripleId` | The counter-triple of a triple | | `multiVaultCalculateAtomId`, `multiVaultCalculateTripleId` | The contract's own identifier derivation | The preview helpers simulate the call from an account, so their configuration also needs a `walletClient`. The identifier helpers return the same values as `@0xintuition/ids`, which needs no network call. ## Read at scale Contract reads answer questions about individual terms. To search, filter, or traverse the graph, query an Intuition Core node, which indexes the chain's events. See the [REST API](https://learn.intuition.box/docs/build/rest-api) and [Query with SQL](https://learn.intuition.box/docs/build/query-with-sql). ## Related - [Networks and contracts](https://learn.intuition.box/docs/build/networks-and-contracts) - [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain) - [Deposit and redeem](https://learn.intuition.box/docs/build/deposit-and-redeem) --- # REST API Every Intuition Core node serves a query API over its knowledge graph, on port 3000 by default. Use it to look up and search atoms and triples, follow the node's activity, and, with an API key, add atoms and triples to the node. An OpenAPI 3.1 description is in the Intuition Core repository, at `docs/openapi.yaml`. The API serves atoms, triples, predicates, and artifacts. It does not serve vaults, share prices, or positions. Read those [from the chain](https://learn.intuition.box/docs/build/read-the-chain) or [with SQL](https://learn.intuition.box/docs/build/query-with-sql). ## Conventions - The examples use a local node at `http://localhost:3000`. - Responses are JSON. A successful response has a `data` member. A list response also has a `pagination` member. - Identifiers are 32-byte values, written as `0x` followed by 64 hexadecimal digits. Send them in lowercase, the form the node stores. - Only records that are active and public are returned. - Lists are ordered newest first. ## Pagination List endpoints accept two query parameters: | Parameter | Default | Meaning | | --------- | ------- | -------------------------------------------------------------- | | `limit` | `25` | Rows per page, from 1 to 100. Larger values are clamped to 100 | | `offset` | `0` | Rows to skip | The `pagination` member repeats `limit` and `offset`, and `count` is the number of rows in this response, not the total. To read every page, increase `offset` by `limit` until `count` is less than `limit`. ```bash curl "http://localhost:3000/api/atoms?limit=100&offset=100" ``` ## Authentication Reads are open by default. Writes require an API key, sent as a bearer token: ```http Authorization: Bearer ik_... ``` The node's operator mints keys and chooses the authentication mode. In `gated` mode, every endpoint except `/health` and `/api/schema` requires a key. See [Write data](https://learn.intuition.box/docs/node/write-data) for keys and modes. A key sent with any request is checked, including a read. An unknown or revoked key returns `401`, even where a key is optional. ## Errors An error response has an `error` member with a code, and sometimes a `message` member with details: ```json { "error": "invalid_input", "message": "body must be { \"input\": string }" } ``` | Status | Code | Meaning | | ------ | ---------------------- | -------------------------------------------------- | | 400 | `invalid_json` | The request body is not valid JSON | | 400 | `invalid_input` | A required member is missing or empty | | 400 | `invalid_term_id` | An identifier is not a 32-byte protocol identifier | | 401 | `api_key_required` | The endpoint requires a key and none was sent | | 401 | `invalid_api_key` | The key is unknown or revoked | | 403 | `api_key_not_writable` | The key is read-only and the request is a write | | 404 | `not_found` | The record is absent or not public | | 413 | `input_too_large` | The atom input exceeds 64,000 characters | | 429 | `rate_limited` | The rate limit is exceeded | | 500 | `internal_error` | An unexpected server error | ## Rate limits Requests are limited per API key, or per client IP address when no key is sent. The default is 120 requests per minute, and an operator can set a different limit for each key. Each response reports the current window: | Header | Meaning | | ----------------------- | -------------------------------------------------------- | | `x-ratelimit-limit` | Requests allowed per minute | | `x-ratelimit-remaining` | Requests left in the current window | | `x-ratelimit-reset` | When the window resets, in milliseconds since Unix epoch | Over the limit, the API returns `429` with a `retry-after` header, in seconds. `/health` is not rate limited. ## Atoms ### `POST /api/atoms` Creates an atom in the node from a URL, a string, or a JSON document. Requires a key. ```bash curl -X POST http://localhost:3000/api/atoms \ -H "Authorization: Bearer ik_..." \ -H 'Content-Type: application/json' \ -d '{"input":"https://github.com/oven-sh/bun"}' ``` ```json { "data": { "id": "0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3", "created": true, "createdBy": "0x1111111111111111111111111111111111111111" } } ``` - `input` is a string of up to 64,000 characters. The node records its raw type: `http_uri` for an `http` or `https` URL, `ipfs_uri` for an `ipfs://` URI, `json` for a JSON object or array, and `string` otherwise. - The identifier is derived from `input` exactly as sent, the same way `calculateAtomId` derives it. The request is idempotent: it returns `201` when the atom is created and `200` with the same identifier afterwards. - `createdBy` is the account the key belongs to. - The node's workers parse, classify, and enrich the atom within seconds. The request stores the atom in the node. It does not send a transaction. To register the atom with the protocol, see [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain). Errors: `400 invalid_json`, `400 invalid_input`, `413 input_too_large`. ### `GET /api/atoms` Lists atoms. | Parameter | Meaning | | --------------------- | ------------------------------------------------------------- | | `limit`, `offset` | Pagination | | `classification_type` | Returns atoms of one classification type only | | `q` | Case-insensitive substring search over the atom's search text | ```bash curl "http://localhost:3000/api/atoms?q=joji&limit=1" ``` ```json { "data": [ { "id": "0x49ed8bdcd1954e1de3af8bb5fca0f0377ebe598de5dd82ab5b4a45a82d53a53f", "rawType": "string", "data": "joji", "isOnchain": true, "classificationType": "Unknown", "parseStatus": "completed", "classificationStatus": "completed", "enrichmentStatus": "skipped" } ], "pagination": { "limit": 1, "offset": 0, "count": 1 } } ``` The example is shortened. Each atom has these members: | Member | Meaning | | --------------------------------------------------------- | --------------------------------------------------------------- | | `id` | The atom's identifier | | `createdAt` | When the node stored the atom | | `isOnchain` | Whether the node recorded the atom from the chain | | `rawType` | `string`, `json`, `http_uri`, or `ipfs_uri` | | `data` | The atom data as text, or `null` | | `dataResolved` | Structured data the workers extracted | | `classificationType` | What the atom refers to, or `Unknown` before classification | | `parseStatus`, `classificationStatus`, `enrichmentStatus` | Each processing stage's state, such as `pending` or `completed` | A new atom matches `q` only after a worker has processed it. ### `GET /api/atoms/:id` Returns one atom with all its members, including the results of each processing stage, and a `stats` member that describes its place in the graph: `inDegree`, `outDegree`, `neighborKindCounts`, and `predicateCounts`. `stats` is `null` until the atom takes part in a triple. Returns `404 not_found` if the atom is absent or not public. ### `GET /api/atoms/:id/artifacts` Returns the enrichment artifacts attached to the atom, newest first, one per provider output. Each artifact has `artifactKind`, such as `opengraph` or `github-repo`, `artifactVersion`, `status`, `sourceUri`, the provider's raw `data`, normalized `extracted` fields, and `error` when the provider failed. Accepts pagination. Returns `404 not_found` if the atom is absent or not public. ### `GET /api/atoms/:id/triples` Returns every triple in which the atom is the subject, the predicate, or the object. Accepts pagination and `expand=terms`, described under [`GET /api/triples`](https://learn.intuition.box/docs/build/rest-api#get-apitriples). ## Triples ### `POST /api/triples` Creates a triple in the node from three term identifiers. Requires a key. ```bash curl -X POST http://localhost:3000/api/triples \ -H "Authorization: Bearer ik_..." \ -H 'Content-Type: application/json' \ -d '{ "subject_id": "0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3", "predicate_id": "0x0840db4575bf6bdb49b66c21dc40cb4cbb5e1b26bd239d7f56b126c14e452c07", "object_id": "0x49ed8bdcd1954e1de3af8bb5fca0f0377ebe598de5dd82ab5b4a45a82d53a53f" }' ``` ```json { "data": { "id": "0x9aaea0e8bbda670c2a1c120dfb4633f974c9ac572ec3e3890331c9e56148e58a", "created": true, "createdBy": "0x1111111111111111111111111111111111111111" } } ``` - The identifier is derived the same way `calculateTripleId` derives it. The request is idempotent, like `POST /api/atoms`. - The node does not check that the three terms exist. Create the atoms first, so that the triple's terms resolve. - The node records all three terms as atoms. Errors: `400 invalid_json`, `400 invalid_input`, `400 invalid_term_id`. ### `GET /api/triples` Lists triples. Filter with `subject_id`, `predicate_id`, and `object_id`, in any combination, and page with `limit` and `offset`. Add `expand=terms` to include a summary of each term in members named `subject`, `predicate`, and `object`. Each summary has `id`, `data`, `classificationType`, and `rawType`. A summary is `null` when its atom is not in the node or not public. Each triple has these main members: | Member | Meaning | | -------------------------------------------- | --------------------------------------------------- | | `id` | The triple's identifier | | `subjectId`, `predicateId`, `objectId` | The identifiers of its terms | | `subjectType`, `predicateType`, `objectType` | `node` for an atom, `triple` for a triple | | `isOnchain` | Whether the node recorded the triple from the chain | | `createdBy` | The account that created it, or `null` | | `createdAt`, `updatedAt` | When the node stored and last updated it | ### `GET /api/triples/:id` Returns one triple. Accepts `expand=terms`. Returns `404 not_found` if the triple is absent or not public. ## Activity, registry, and metadata | Endpoint | Returns | | ------------------------- | ---------------------------------------------------------------------------------------------------- | | `GET /api/events` | The append-only activity log, newest first. Filter with `entity_kind`, `event_type`, and `entity_id` | | `GET /api/predicates` | The node's predicate registry, ordered by slug | | `GET /api/stats` | Counts of public atoms and triples, and of all accounts and predicates | | `GET /api/stats/pipeline` | Counts by status for each processing stage | | `GET /api/schema` | Table, column, key, and index metadata for the knowledge graph schema | | `GET /health` | `{"status":"ok"}`, or `503` when the database is unreachable | `entity_kind` is `node`, `triple`, `predicate`, or `artifact`. Each event has `eventTime`, `eventType`, `entityKind`, `entityId`, `actorId`, `isOnchain`, `blockNumber`, `txHash`, and a `payload`. `GET /api/events` accepts pagination. `GET /api/schema` is available without a key in every authentication mode, so that tools can discover the schema. ## Common tasks | Task | Request | | --------------------------------------- | ----------------------------------------------------- | | Search atoms by text | `GET /api/atoms?q=bun` | | List the claims about an atom, readably | `GET /api/atoms/:id/triples?expand=terms` | | List the claims that use one predicate | `GET /api/triples?predicate_id=0x...&expand=terms` | | Find who made a claim about an object | `GET /api/triples?predicate_id=0x...&object_id=0x...` | | Read an atom's metadata from the web | `GET /api/atoms/:id/artifacts` | | Follow what the node records | `GET /api/events` | ## Related - [Query with SQL](https://learn.intuition.box/docs/build/query-with-sql) - [Atom services](https://learn.intuition.box/docs/build/atom-services) - [Data model](https://learn.intuition.box/docs/reference/data-model) --- # Atoms An atom is the basic unit of Intuition data. It identifies one thing, such as a person, a repository, a song, a concept, or an address. Triples are built from atoms, and every atom registered onchain has a vault. ## Atom data An atom is defined by its data, a sequence of bytes. The protocol places no requirement on what the bytes mean. In practice, atom data takes one of four forms. The last column shows the `raw_type` that Intuition Core records for each form. | Form | Example | Stored in Core as | | ------------- | ----------------------------------------------------------------- | ----------------- | | URL | `https://github.com/oven-sh/bun` | `http_uri` | | IPFS URI | `ipfs://bafybei...` | `ipfs_uri` | | JSON document | `{"@context":"https://schema.org/","@type":"Book","name":"Dune"}` | `json` | | Plain string | `hello` | `string` | A value that begins with `0x` and contains only hexadecimal digits is read as raw bytes rather than as text. See [Identifiers](https://learn.intuition.box/docs/concepts/identifiers#text-and-bytes). ## Size limit The `MultiVault` contract rejects empty atom data and atom data longer than a configured maximum. At the time of writing, the maximum on mainnet is 1,000 bytes. Read the current value from `atomDataMaxLength` in the contract's `getGeneralConfig` result. To refer to a larger document, store the document elsewhere, for example on IPFS, and use its URI as the atom data. ## Identity An atom's identifier is derived from its data and nothing else. Two atoms with the same data are the same atom. This has three consequences: - An atom cannot be registered twice. The second attempt fails. - Anyone who knows the data can derive the identifier without asking anyone. - Data that differs by a single byte produces an unrelated identifier. The third point matters when you choose atom data. `https://example.com` and `https://example.com/` are different atoms. To converge on the same atom as other applications, use the canonical forms described in [Classification and enrichment](https://learn.intuition.box/docs/concepts/classification-and-enrichment). ## Choosing atom data Use the most stable and most widely recognized reference to the thing you mean: - For something with a canonical URL, use that URL. - For something with a standard identifier, such as an ISBN or an ISRC, use the identifier. - For structured data, use a schema.org JSON document. The `buildAtomData` function in `@0xintuition/classifications` produces the same document for the same input. - For a concept that has no better reference, such as a tag, use a plain string. ## Atoms in Intuition Core Core stores atoms in the `kg.nodes` table. Each row holds the atom's identifier, its original data, whether the atom has been observed onchain, and the state of the parse, classify, and enrich stages. See the [Data model](https://learn.intuition.box/docs/reference/data-model). ## Related - [Triples](https://learn.intuition.box/docs/concepts/triples) link atoms into claims. - [Identifiers](https://learn.intuition.box/docs/concepts/identifiers) describes the derivation. --- # Classification and enrichment Atom data is a sequence of bytes. To be useful in an application, an atom needs to be understood: this URL is a GitHub repository, this string is an ENS name, this JSON document describes a song. Intuition Core does this in three stages. | Stage | Question it answers | Output | | -------- | ----------------------------- | ------------------------------------------------- | | Parse | What form does the data take? | A kind, such as `url`, `json`, or `ens_name` | | Classify | What does it refer to? | A domain and subtype, such as `github` and `repo` | | Enrich | What is known about it? | Artifacts, such as titles, images, and metadata | Each atom moves through the stages in order. The state of each stage is recorded on the atom as `pending`, `processing`, `completed`, `failed`, or `skipped`. ## Parse The parser examines the raw input and assigns one kind, in this order of precedence: IPFS URI, Ethereum address, ENS name, JSON, URL, ISBN, and plain string. For URLs and IPFS URIs, it can also inspect the remote content to determine whether it is a web page, an image, a JSON document, and so on. ## Classify Plugins perform classification. Each plugin recognizes one domain, such as GitHub, Spotify, Wikipedia, or X. A plugin's classifier decides whether the input belongs to its domain. Its resolver then produces a canonical description of the thing: a schema.org type, a canonical identifier, and `sameAs` links. Classification must be **deterministic**. The same input always produces the same result, with no dependence on time, randomness, or an unpinned model. This is what lets two applications that describe the same thing arrive at the same atom. ## Enrich Enrichment plugins each fetch metadata from one source and return typed artifacts. Many sources work without credentials, including OpenGraph, Wikipedia, Wikidata, npm, and public GitHub data. Others need an API key. Without its key, a provider either runs a reduced keyless lookup, as Spotify does, or records an error for itself, as TMDB, YouTube, and X do. In either case, the run continues and the other providers' artifacts are stored. [Enrichment providers](https://learn.intuition.box/docs/extend/enrichment-providers) lists which providers work without a key. Core stores artifacts with the atom and returns them from the REST API. ## Using the pipeline without a database The same engines are available as a stateless HTTP service, atom services. Send an input and receive its classification, with nothing stored: ```bash curl -X POST http://localhost:4010/v1/classify \ -H 'Content-Type: application/json' \ -d '{"input":"https://github.com/vercel/next.js"}' ``` The response includes these fields, among others: ```json { "ok": true, "status": "complete", "message": "Resolved by github-resolver with 1 candidate atom.", "classification": { "type": "url", "domain": "github", "subtype": "repo", "confidence": 0.97 } } ``` To classify and enrich in one call, use `/v1/process`. See [Atom services](https://learn.intuition.box/docs/build/atom-services). ## Adding coverage for a new domain Classification and enrichment are both plugin systems. The built-in plugins use the same interface that is available to you. See [Classification plugins](https://learn.intuition.box/docs/extend/classification-plugins) and [Enrichment plugins](https://learn.intuition.box/docs/extend/enrichment-plugins). ## Related - [Atoms](https://learn.intuition.box/docs/concepts/atoms) - [Architecture](https://learn.intuition.box/docs/reference/architecture) --- # Identifiers Every term in Intuition has a 32-byte identifier, written as `0x` followed by 64 hexadecimal digits. Identifiers are derived from content. No server, counter, or contract assigns them. ## Derivation Each identifier is a keccak256 hash of packed values (`abi.encodePacked`): | Term | Identifier | | -------------- | ---------------------------------------------------------- | | Atom | `keccak256(ATOM_SALT, keccak256(data))` | | Triple | `keccak256(TRIPLE_SALT, subjectId, predicateId, objectId)` | | Counter-triple | `keccak256(COUNTER_SALT, tripleId)` | The salts are protocol constants. Each one is the keccak256 hash of its own name, for example `keccak256("ATOM_SALT")`. The `MultiVault` contract uses the same derivation, so an identifier computed in an application is the identifier the contract registers. ## Deriving identifiers in code Use `@0xintuition/ids`. Do not reimplement the derivation. ```ts import { calculateAtomId, calculateCounterTripleId, calculateTripleId, } from '@0xintuition/ids'; const atomId = calculateAtomId('hello'); // 0xa0e157e5fa1b17d3b54ec73622ce3317296920a06502661617613d59f58e947e const tripleId = calculateTripleId( calculateAtomId('hello'), calculateAtomId('is a'), calculateAtomId('greeting'), ); const counterTripleId = calculateCounterTripleId(tripleId); ``` The package also exports the salts (`ATOM_SALT`, `TRIPLE_SALT`, and `COUNTER_SALT`), validators such as `isValidAtomId` and `isValidTripleId`, and the display helper `shortenHex`. The validators check only that a value is a well-formed 32-byte identifier. They do not check how it was derived. ## Text and bytes `calculateAtomId` accepts a string. How the string is interpreted depends on its form. | Input | Interpreted as | | ----------------------------------------------------- | -------------- | | A string that is valid hexadecimal with a `0x` prefix | Raw bytes | | Any other string | UTF-8 text | The two inputs below are therefore the same atom, because `0x68656c6c6f` is the UTF-8 encoding of `hello`: ```ts calculateAtomId('hello'); calculateAtomId('0x68656c6c6f'); // Both: 0xa0e157e5fa1b17d3b54ec73622ce3317296920a06502661617613d59f58e947e ``` > [!WARNING] > Do not pass hexadecimal input with an odd number of digits, such as `0xabc`. > `@0xintuition/ids` reads it as `0x0abc`, but the transaction builder in > `@0xintuition/protocol` encodes it as `0xabc0`. The identifier you derive > would not match the one created onchain. Use an even number of digits. ## Whitespace and normalization The derivation hashes exactly the bytes it is given. Leading and trailing whitespace, letter case, Unicode normalization form, and, in a JSON document, key order and spacing all change the result. Normalize input before you derive an identifier, and apply the same normalization everywhere. ## Why identifiers do not change Because an identifier depends only on content: - Publishing a term onchain does not change its identifier. - Two parties who describe the same thing with the same data produce the same identifier without coordinating. - A node can store a term before it is registered onchain. When the registration is indexed, it carries the same identifier, so the node keeps one record for the term. The derivation is fixed. Changing it would give every term a new identifier, which would split the graph. Treat the functions in `@0xintuition/ids` as the definition. ## Related - [Atoms](https://learn.intuition.box/docs/concepts/atoms) and [Triples](https://learn.intuition.box/docs/concepts/triples) - [Read the chain](https://learn.intuition.box/docs/build/read-the-chain) --- # Predicates A predicate names the relationship a triple expresses, such as `has tag`, `same as`, or `created by`. A predicate is an atom. What makes it useful is that everyone uses the same one. ## Canonical predicates If two applications express the same relationship with different predicate atoms, their triples do not line up. To prevent this, Intuition publishes a canonical predicate registry in `@0xintuition/predicates`. Each canonical predicate has: - a **name**, such as `has tag`, - a **description** that fixes its meaning, - **atom data**, a small schema.org `DefinedTerm` document built from the name and description, and - an **identifier**, derived from that atom data. Because the atom data is fixed, the identifier is fixed. Every application that imports the predicate uses the same identifier. ```ts import { atomData, id, predicate } from '@0xintuition/predicates/hasTag'; predicate.name; // 'has tag' id; // The predicate's atom identifier atomData; // The JSON document the identifier is derived from ``` Import a single predicate from its own entry point, as above, to keep application bundles small. Like any atom, a canonical predicate must be registered onchain before an onchain triple can use it. Check whether it is registered, and create it if it is not. See [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain). ## The launch set The registry marks a launch set of predicates intended for general use: | Area | Predicates | | -------------- | -------------------------------------------------------------------------- | | Classification | `has tag`, `has type`, `has category`, `has description` | | Identity | `same as`, `linked account`, `url`, `imgUrl` | | Lists | `contain`, `listed in`, `curated by` | | Social | `follow`, `like`, `trust`, `distrust`, `endorse`, `recommend`, `vouch for` | | Opinion | `agree with`, `disagree with`, `bullish on`, `bearish on` | | Comparison | `better than`, `alternative to` | | Authorship | `created by` | The registry contains many more predicates than the launch set. Each record also carries semantic flags, such as whether the predicate is symmetric or transitive, and its inverse where one exists. ## Choosing a predicate - Prefer a canonical predicate. Introduce a new one only when none fits. - Read the description, not only the name. `has tag` is for informal keywords. `has type` and `has category` are for structured classification. - Check the direction. In `A created by B`, the subject is the thing that was created. ## Predicates in Intuition Core A node keeps its own predicate registry, with a label and semantic flags for each entry. It seeds 14 baseline predicates, such as `references`, `follows`, and `has-tag`, when its database is first migrated. Each entry is identified by a slug, and not every entry corresponds to a canonical predicate atom. A triple's predicate is always a term identifier. To list the registry: ```bash curl http://localhost:3000/api/predicates ``` ## Related - [Triples](https://learn.intuition.box/docs/concepts/triples) - [Packages](https://learn.intuition.box/docs/build/packages) --- # Triples A triple states a relationship between three terms: a **subject**, a **predicate**, and an **object**. | Subject | Predicate | Object | | -------------------------------- | --------- | ----------------- | | `https://github.com/oven-sh/bun` | `has tag` | `runtime` | | `vitalik.eth` | `same as` | `Vitalik Buterin` | | `ETHGlobal` | `has tag` | `hackathon` | The subject and object are usually atoms. The predicate is also an atom, drawn from a shared vocabulary. See [Predicates](https://learn.intuition.box/docs/concepts/predicates). ## Identity A triple's identifier is derived from the identifiers of its three terms, in order. Like an atom, a triple cannot be registered twice, and anyone can derive its identifier from its parts. Order is significant. `A follows B` and `B follows A` are different triples with different identifiers. ## Registration order The `MultiVault` contract registers a triple only if its subject, predicate, and object are already registered. To publish a triple, register any of its terms that are missing first, then the triple. See [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain). ## Triples about triples A triple is a term, so it can be the subject or object of another triple. This lets you make a claim about a claim, for example to attach a source or a qualification. Core records, for each position of a triple, whether it refers to an atom or to another triple. ## Counter-triples Every triple has a counter-triple, which represents disagreement with it. The counter-triple is created together with the triple. It has its own identifier, derived from the triple's identifier, and its own vault. Depositing on a triple expresses support for the claim. Depositing on its counter-triple expresses opposition. ```ts import { calculateCounterTripleId } from '@0xintuition/ids'; // tripleId is the identifier of an existing triple. const counterTripleId = calculateCounterTripleId(tripleId); ``` ## A triple is a claim, not a fact Registering a triple records that someone made the claim. It does not make the claim true. Whether a claim is credible is expressed by who backs it and by how much. See [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals). ## Reading triples Core indexes every triple by all six orderings of subject, predicate, and object. As a result, one request returns every triple that mentions a given atom in any position. Replace `` with the atom's identifier: ```bash curl http://localhost:3000/api/atoms//triples ``` See the [REST API](https://learn.intuition.box/docs/build/rest-api). ## Related - [Identifiers](https://learn.intuition.box/docs/concepts/identifiers) - [Predicates](https://learn.intuition.box/docs/concepts/predicates) --- # Vaults and signals Registering a term records that it exists. It says nothing about whether anyone considers it important or true. That information comes from vaults. ## Vaults Every registered atom, triple, and counter-triple has a vault in the `MultiVault` contract. Anyone can deposit TRUST, the network's native currency, into a vault and receive shares in return. Redeeming shares returns TRUST, minus fees. A deposit is a **signal**: a public, costly statement that the depositor stands behind the term. - A deposit on an atom signals that the thing it identifies matters. - A deposit on a triple signals agreement with its claim. - A deposit on a counter-triple signals disagreement. Redeeming shares withdraws the signal. Because a signal costs something to make, it carries more weight than a free rating. You cannot hold shares in a triple and in its counter-triple on the same bonding curve. To change sides, redeem your shares on one side first. ## Bonding curves A bonding curve sets the price of a vault's shares. A term has a separate vault on each curve, and each curve has a numeric curve ID. The `BondingCurveRegistry` contract lists the available curves. On mainnet, they are: | Curve ID | Curve | How the share price moves | | -------- | ------------------------ | ------------------------------------------ | | 1 | Linear Curve (default) | Rises only as fees accumulate in the vault | | 2 | Offset Progressive Curve | Rises as more shares are issued | The vault on the default curve is created together with the term. A vault on any other curve is created by its first deposit. On a curve whose price rises with supply, earlier depositors receive more shares for the same amount than later ones. To preview the outcome of a deposit, use `@0xintuition/curves`, which needs no network call, or call the contract's `previewDeposit` and `previewRedeem` functions. ## Fees Deposits and redemptions can incur the following fees. The rates are contract configuration, so read them from the chain with `getVaultFees`, `getAtomConfig`, and `getTripleConfig` rather than hard-coding them. | Fee | Charged on | Goes to | | ----------------------- | -------------------------------------- | ------------------------------------------------------ | | Protocol fee | Deposits and redemptions | The protocol | | Entry fee | Deposits | The term's vault on the default curve | | Exit fee | Redemptions | The term's vault on the default curve | | Atom wallet deposit fee | Deposits on an atom | The atom's wallet | | Atom deposit fraction | Deposits on a triple or counter-triple | The vaults of the triple's three terms, in equal parts | Each atom has an atom wallet, a smart contract account linked to the atom. The fees it collects are paid to the wallet's owner when claimed. Entry and exit fees apply only while the term's vault on the default curve holds at least a threshold number of shares (`feeThreshold` in the contract's general configuration), so the first deposits into a new vault do not pay them. The atom deposit fraction applies only when the default-curve vaults of all three of the triple's terms meet the same threshold. Two consequences follow. Holding shares in a vault that others later deposit into increases the value of those shares. And activity on a triple rewards the holders of the terms it is built from. ## The cost of creating a term Creating a term requires a minimum payment. It covers a fixed creation fee and a small number of initial shares, which the contract issues to a burn address so that a new vault is never empty. The minimum for a triple covers initial shares for both the triple and its counter-triple. Any amount above the minimum is deposited into the new vault on your behalf, minus fees. Read the current amounts from the contract rather than hard-coding them. In this example, `address` is the `MultiVault` address and `publicClient` is a viem public client, set up as in [Read the chain](https://learn.intuition.box/docs/build/read-the-chain): ```ts import { multiVaultGetAtomCost, multiVaultGetTripleCost, } from '@0xintuition/protocol'; const atomCost = await multiVaultGetAtomCost({ address, publicClient }); const tripleCost = await multiVaultGetTripleCost({ address, publicClient }); ``` ## Collecting fees in your application An application that routes deposits through its own contract can add a fee of its own. See [Fee proxy](https://learn.intuition.box/docs/build/fee-proxy). ## Signals in Intuition Core When a node indexes the chain, its projections maintain read models for vaults, positions, share prices, and signals. Core records every deposit and redemption as a signal for the account, term, and curve involved. See the [Data model](https://learn.intuition.box/docs/reference/data-model#event-store-tables). ## Related - [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain) - [Read the chain](https://learn.intuition.box/docs/build/read-the-chain) --- # Classification plugins The classification engine is built from plugins. The built-in plugins, for GitHub, Spotify, Wikipedia, and others, use the same interface that is available to you. A plugin adds coverage for a domain, such as scientific papers or an internal system, and produces the same canonical atoms for the same input on every node. ## Before you begin - A clone of the [Intuition Core repository](https://github.com/0xIntuition/intuition-core), with its dependencies installed (`bun install`). The plugin packages are workspace packages of that repository. They are not published to npm. - Familiarity with [Classification and enrichment](https://learn.intuition.box/docs/concepts/classification-and-enrichment). The repository includes a complete example at `packages/atom-classification-example-plugin`. Copy it to start a new plugin package in `packages/`. The example below also uses `zod`, so add `"zod": "catalog:"` to your package's dependencies, as the other Core packages do. ## Structure A plugin is an object with a manifest, classifiers, and resolvers. This plugin recognizes inputs such as `doi:10.1000/182` and resolves each one to a canonical scholarly article. ```ts import type { AtomClassificationPlugin } from '@0xintuition/atom-classification'; import { z } from 'zod/v4'; const PLUGIN_ID = 'example-doi'; // DOIs are case-insensitive, so lowercase them to get one canonical form. function parseDoi(input: string): string | null { const value = input.trim(); if (!value.toLowerCase().startsWith('doi:')) return null; const doi = value.slice('doi:'.length).trim().toLowerCase(); return doi.length > 0 ? doi : null; } export function createDoiPlugin(): AtomClassificationPlugin { return { manifest: { id: PLUGIN_ID, version: '1.0.0', engineRange: '^0.1.0', runtime: 'universal', capabilities: ['classifier:text:paper', 'resolver:research:paper'], permissions: [], dependsOn: ['type-profiles'], provides: ['research:paper'], priority: 30, }, // Register any schema.org type that the built-in profiles do not cover. registerTypes(registry) { registry.register({ type: 'ScholarlyArticle', category: 'thing', schema: z.object({ name: z.string().min(1) }).passthrough(), requiredFields: ['name'], recommendedFields: ['sameAs'], identityFields: ['sameAs'], }); }, classifiers: [ { id: `${PLUGIN_ID}-classifier`, priority: 30, classify: (input) => { const doi = parseDoi(input); if (!doi) return null; return { type: 'text', domain: 'research', subtype: 'paper', confidence: 0.9, meta: { plugin: PLUGIN_ID, doi }, }; }, }, ], resolvers: [ { id: `${PLUGIN_ID}-resolver`, priority: 30, canResolve: (classification) => classification.domain === 'research' && classification.subtype === 'paper', resolve: ({ request }) => { const doi = parseDoi(request.input); if (!doi) return null; return { atoms: [ { schemaType: 'ScholarlyArticle', category: 'thing', title: `DOI ${doi}`, canonicalId: `doi:${doi}`, sameAs: [`https://doi.org/${doi}`], source: PLUGIN_ID, }, ], fallbackUsed: false, }; }, }, ], }; } ``` ### Manifest | Field | Meaning | | -------------- | ---------------------------------------------------------------------------------------------------------- | | `id` | A unique, lowercase, hyphenated identifier | | `version` | The plugin's semantic version | | `engineRange` | The engine versions the plugin supports. The current engine version is `0.1.0` | | `runtime` | Where the plugin can run: `server`, `client`, or `universal`, which covers both | | `capabilities` | Free-form labels for what the plugin does | | `permissions` | `network` if the plugin makes network requests, `ai` if it calls a model. A client runtime cannot use `ai` | | `dependsOn` | Plugins that must be registered first. The engine fails if one is missing | | `provides` | Free-form labels for what the plugin produces | | `priority` | Lower values run earlier. Built-in plugins mostly use values from 10 to 50 | ### Classifiers A classifier receives the trimmed input and returns a classification, or `null` to decline. A classification has a `type` (such as `url` or `text`), a `domain`, a `subtype`, a `confidence` between 0 and 1, and `meta`. Every classifier sees the input. The engine keeps the classification with the highest `confidence`. When two are equal, the classifier with the lower `priority` wins. A classifier that throws is ignored. ### Resolvers A resolver turns a classification into candidate atoms. The engine calls `resolve` only when `canResolve` returns `true`. The context passed to `resolve` contains the original `request`, the `classification`, and the time as `now`. Each candidate atom has a schema.org `schemaType`, a `category`, and a `title`. Give it a `canonicalId` and `sameAs` links too. These are what make two applications that describe the same thing converge on one atom. The `schemaType` must be registered with the engine. The built-in `type-profiles` plugin registers common types such as `Person`, `MusicRecording`, `Book`, and `WebSite`. Register any other type in your plugin's `registerTypes`, as the example does. A resolver that returns an unregistered type fails. ## Test the plugin Create an engine with the default plugins and yours, and classify an input: ```ts import { createClassificationEngine, defaultClassificationPreset, } from '@0xintuition/atom-classification'; import { createDoiPlugin } from './index'; const engine = createClassificationEngine({ runtime: 'server', plugins: [...defaultClassificationPreset(), createDoiPlugin()], }); const result = await engine.classify({ input: 'doi:10.1000/182' }); console.log(result.classification); // domain 'research', subtype 'paper' console.log(result.resolved); // the ScholarlyArticle candidate ``` Write tests with `bun:test` and run them with `bun test`. The example plugin and the built-in plugins show the conventions. ## Add the plugin to your node Atom services and the classification worker build their engine in the same file, `services/atom-services/src/service/runtime.ts`. A plugin you add there classifies both direct requests and every atom that passes through the node. 1. Add your package to the dependencies in `services/atom-services/package.json`, for example `"@0xintuition/example-doi": "workspace:*"`, and run `bun install` from the repository root. 2. In `runtime.ts`, find `createClassificationRuntime` and append your plugin to the plugin list: ```ts plugins: [ ...defaultClassificationPreset({ // The existing options stay as they are. }), createDoiPlugin(), ], ``` 3. Rebuild and restart the services: ```bash docker compose up -d --build atom-services workers-classification ``` 4. Classify an input through atom services: ```bash curl -X POST http://localhost:4010/v1/classify \ -H 'Content-Type: application/json' \ -d '{"input":"doi:10.1000/182"}' ``` The response's `classification` has the domain `research` and the subtype `paper`. ## Requirements - **Be deterministic.** Classification feeds identity. The same input must always produce the same output. Do not use randomness, the current time, or an unpinned model in the classification path. - **Degrade gracefully.** If the plugin needs a credential or the network and it is unavailable, return `null`. Do not throw for a missing credential. - **Declare dependencies.** If the plugin builds on another plugin's output or types, name that plugin in `dependsOn`. ## Contribute a plugin A plugin that is useful beyond your node can become part of Intuition Core. Classification output is identity-sensitive, so open an issue in the Core repository and agree on the scope before you open a pull request. See the repository's `CONTRIBUTING.md`. ## Related - [Enrichment plugins](https://learn.intuition.box/docs/extend/enrichment-plugins) - [Classification and enrichment](https://learn.intuition.box/docs/concepts/classification-and-enrichment) - [Atom services](https://learn.intuition.box/docs/build/atom-services) --- # Enrichment plugins An enrichment plugin reads one source and returns **artifacts**, typed records that are attached to an atom. The built-in providers, such as OpenGraph, Wikipedia, and npm, use the interface described here. ## Before you begin - A clone of the [Intuition Core repository](https://github.com/0xIntuition/intuition-core), with its dependencies installed (`bun install`). Write the plugin as a workspace package in `packages/`, with `@0xintuition/atom-enrichment` and `zod` as dependencies. The enrichment packages are not published to npm. - An understanding of how classification feeds enrichment. See [Classification and enrichment](https://learn.intuition.box/docs/concepts/classification-and-enrichment). The npm provider, at `packages/atom-enrichment/src/plugins/providers/npm`, is a compact, complete example that needs no API key. ## Structure This plugin enriches atoms whose URL points at a record on a fictional service, `example.org`. It needs an API key, and declines when the key is absent. ```ts import { type ClassificationDefinition, defineEnrichmentPlugin, type EnrichmentPlugin, type EnrichmentRequest, } from '@0xintuition/atom-enrichment'; import { z } from 'zod/v4'; const PLUGIN_ID = 'example-records'; const ARTIFACT_TYPE = 'example-record'; const recordSchema = z.object({ id: z.string(), title: z.string(), description: z.string().optional(), }); // The artifact type, registered with the engine when you add the plugin. export const exampleRecordArtifactType: ClassificationDefinition = { slug: ARTIFACT_TYPE, displayName: 'Example record', category: 'knowledge', dataSchema: recordSchema, }; function parseRecordId(request: EnrichmentRequest): string | undefined { const url = request.input.hints?.url; const match = url?.match(/^https:\/\/example\.org\/records\/([a-z0-9-]+)$/); return match?.[1]; } export function createExampleRecordsPlugin( options: { apiKey?: string } = {}, ): EnrichmentPlugin { return defineEnrichmentPlugin({ id: PLUGIN_ID, version: '1.0.0', runtime: 'universal', artifactTypes: [ARTIFACT_TYPE], priority: 40, TTL: 3_600, supports(request) { return Boolean(options.apiKey) && parseRecordId(request) !== undefined; }, async enrich(request, ctx) { const id = parseRecordId(request); if (!id || !options.apiKey) return []; const response = await fetch(`https://api.example.org/records/${id}`, { headers: { authorization: `Bearer ${options.apiKey}` }, signal: ctx.signal, }); if (response.status === 404) return []; if (!response.ok) { throw new Error(`HTTP ${response.status} from api.example.org`); } const record = recordSchema.parse(await response.json()); return [ { artifact_type: ARTIFACT_TYPE, data: record, meta: { pluginId: PLUGIN_ID, provider: PLUGIN_ID, identityStrength: 'identifier', fetchedAt: ctx.now(), sourceUrl: `https://example.org/records/${id}`, }, }, ]; }, }); } ``` ### Plugin fields | Field | Meaning | | --------------- | --------------------------------------------------------------------------------------------------------- | | `id` | A unique identifier: 2 to 40 lowercase letters, digits, and hyphens, starting with a letter | | `version` | The plugin's semantic version | | `runtime` | `server`, `client`, or `universal`, which covers both | | `artifactTypes` | The artifact types the plugin can return. Each follows the same naming rule as `id` | | `priority` | Lower values run earlier. The default is `100` | | `TTL` | How long, in seconds, the engine caches the plugin's artifacts. Omit it to disable caching for the plugin | | `supports` | A fast check of whether the plugin applies to the request. It must not make network requests | | `enrich` | Fetches the data and returns artifacts. Return an empty array when there is nothing to return | ### The request `supports` and `enrich` receive the classified atom in `request.input`: | Field | Contents | | ---------- | ------------------------------------------------------------------------------ | | `atomType` | The atom's category, such as `person`, `song`, or `software` | | `jsonLd` | The schema.org document that classification produced | | `hints` | Optional `name`, `description`, `url`, `identifiers`, and `locale` | | `targets` | Parsed targets for some platforms, such as a GitHub repository or an X profile | ### The context `enrich` also receives a context: | Member | Purpose | | --------- | ------------------------------------------------------------------------------ | | `now()` | The current time as an ISO 8601 string. Use it for `meta.fetchedAt` | | `signal` | An abort signal. Enrichment runs under a deadline, so pass it to every request | | `logger` | An optional logger with `debug`, `warn`, and `error` | | `secrets` | Optional secrets supplied by the host | ### Artifacts Each artifact has an `artifact_type`, its `data`, and a `meta` object. `meta.pluginId`, `meta.provider`, and `meta.fetchedAt` are required. `meta.sourceUrl`, `meta.confidence`, and `meta.identityStrength` are optional. The engine validates `data` against the schema registered for the artifact type. An artifact whose type is not registered, or whose data does not match, is dropped and reported as a `validation_error`. Register your artifact type when you add the plugin to your node. ## Requirements 1. **Validate output with a schema.** Parse the source's response before returning it. Unvalidated data from a third party should not enter the graph. 2. **Treat credentials as optional.** Accept keys as options. If a key is absent, return `false` from `supports`. Never throw for a missing credential. This is what allows a node to run without paid accounts. 3. **Honor `ctx.signal`.** Pass the abort signal to every request. 4. **Fail softly.** Treat a 404 as an empty result. Throw for an unexpected failure. The engine records the error for your plugin alone and still stores the other plugins' artifacts. It classifies the error by its message: timeouts, rate limits (`429`), and upstream errors (a message that mentions `HTTP`) are marked retriable. 5. **Set an accurate `TTL`.** Use a short value for volatile data, such as prices, and a long one for stable data, such as publication metadata. 6. **Read one source per plugin.** Combine sources in a preset, not inside a plugin. ## Identity strength Set `meta.identityStrength` to describe how the plugin found the resource. | Value | Use when the resource was found by | | ------------ | ---------------------------------- | | `identifier` | An external identifier | | `url` | A direct URL to the resource | | `title` | A search by name | A search by name is the weakest of the three, because a name can match the wrong thing. An artifact found by name can contribute only descriptive fields, such as names, descriptions, images, and dates. The full list is exported as `TITLE_STRENGTH_DESCRIPTIVE_FIELDS` from `@0xintuition/atom-enrichment/extraction`. Every other field, such as an ISBN or a repository URL, is treated as identity-bearing, and a search by name cannot supply it. An artifact without `identityStrength` is treated as `identifier`. ## Add the plugin to your node Atom services and the enrichment worker build their engine in the same file, `services/atom-services/src/service/runtime.ts`. A plugin you add there serves both `POST /v1/enrich` and every atom that passes through the node. 1. Add your package to the dependencies in `services/atom-services/package.json` and run `bun install` from the repository root. 2. In `runtime.ts`, add the plugin to the default preset. Find `createPresetFactories` and extend `allProviders`: ```ts const allProviders = () => [ ...serverDefaultPreset(serverDefaultPresetOptions), createExampleRecordsPlugin({ apiKey: env.EXAMPLE_RECORDS_API_KEY }), ]; ``` 3. In the same file, register the artifact type. Create a registry that contains the built-in types and yours, and pass it to `createEnrichmentEngine` in `createEnrichmentRuntime`: ```ts import { createDefaultClassificationRegistry } from '@0xintuition/atom-enrichment'; const artifactTypes = createDefaultClassificationRegistry(); artifactTypes.register(exampleRecordArtifactType); // In createEnrichmentRuntime, inside createEngine: createEnrichmentEngine({ plugins: presetFactories[preset](), cache: cacheRuntime.enrichmentCache, classifications: artifactTypes, // The existing identity option stays as it is. }); ``` 4. Add `EXAMPLE_RECORDS_API_KEY` to `.env`. Docker Compose passes only the variables listed in `docker-compose.yml`, so also add it to the `environment` of `atom-services` and of the workers. 5. Rebuild and restart the services: ```bash docker compose up -d --build atom-services workers-enrichment ``` 6. Process an input your plugin supports: ```bash curl -X POST http://localhost:4010/v1/process \ -H 'Content-Type: application/json' \ -d '{"rawInput":"https://example.org/records/123"}' ``` The artifact appears in the response's `enrichment.artifacts`. If it does not, look for your plugin in `enrichment.skipped` and `enrichment.errors`. For atoms processed by the workers, artifacts are stored in `kg.artifacts`, with the artifact type in `artifact_kind`. > [!IMPORTANT] > The enrichment worker keeps only OpenGraph, favicon, and brand artifacts for > an atom classified as a generic `WebSite`, which is the fallback for any URL > that no classifier recognizes. Atom services do not apply this rule, so a > plugin can work in `/v1/process` and still produce nothing in the workers. > If your source's URLs fall back to `WebSite`, write a > [classification plugin](https://learn.intuition.box/docs/extend/classification-plugins) that gives them a > specific type. ## Related - [Classification plugins](https://learn.intuition.box/docs/extend/classification-plugins), for sources that imply a type as well as metadata - [Enrichment providers](https://learn.intuition.box/docs/extend/enrichment-providers) - [Atom services](https://learn.intuition.box/docs/build/atom-services) --- # Enrichment providers A node enriches atoms from the providers on this page. All of them are in the default preset. Some work without configuration, and the rest need an API key. Every key is optional. ## Available without a key | Provider | Plugin IDs | What it provides | | ---------------------------------------- | ---------------------------------- | ---------------------------------------------------------------- | | OpenGraph, microdata and JSON-LD, oEmbed | `opengraph`, `microdata`, `oembed` | Titles, descriptions, images, and embeddable media for web pages | | Favicons | `favicon` | Site icons | | Wikipedia and Wikidata | `wikipedia`, `wikidata` | Article extracts, canonical entities, and `sameAs` links | | GitHub | `github` | Repositories and profiles, at unauthenticated rate limits | | npm | `npm` | Package metadata and weekly downloads | | Crossref | `crossref` | Scholarly works, by DOI | | MusicBrainz and Apple Music | `musicbrainz`, `apple-music` | Artists, releases, and recordings | | Open Library | `openlibrary` | Books | | CoinGecko | `coingecko` | Token prices and market data, at public rate limits | | Spotify | `spotify` | Titles and artwork from Spotify's public oEmbed | ## Enabled by a key | Variables | Provider | Enables | Get a key from | | ----------------------------------------------------------------------------- | --------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `GITHUB_TOKEN` | GitHub | Higher rate limits: 5,000 requests an hour instead of 60 | GitHub, under **Settings > Developer settings > Personal access tokens**. A read-only fine-grained token is enough | | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, and optionally `SPOTIFY_MARKET` | Spotify | Full track, album, artist, and playlist metadata | [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard) | | `ETHERSCAN_API_KEY` | Etherscan | Contract names, token metadata, and verified source | [etherscan.io/apis](https://etherscan.io/apis) | | `TMDB_API_KEY` | TMDB | Films, television series, and credits | [themoviedb.org/settings/api](https://www.themoviedb.org/settings/api) | | `COINGECKO_API_KEY` | CoinGecko | The rate limits of a CoinGecko Demo plan | [coingecko.com/en/api](https://www.coingecko.com/en/api) | | `YOUTUBE_API_KEY` | YouTube | Video metadata | [Google Cloud Console](https://console.cloud.google.com/), with the YouTube Data API v3 enabled | | `GOOGLE_PLACES_API_KEY` | Places | Businesses and places | [Google Cloud Console](https://console.cloud.google.com/), with the Places API enabled | | `X_BEARER_TOKEN` | X | X profiles | [developer.x.com](https://developer.x.com/) | | `BRANDFETCH_API_KEY` | Brand | Logos and brand assets from Brandfetch | [brandfetch.com/developers](https://brandfetch.com/developers) | | `PODCAST_INDEX_API_KEY`, `PODCAST_INDEX_API_SECRET` | Podcast Index | Podcasts and episodes | [api.podcastindex.org](https://api.podcastindex.org/) | | `CANOPY_API_KEY` | Product listing | Amazon product listings | [canopyapi.co](https://www.canopyapi.co/) | The Spotify, X, Etherscan, and Canopy credentials also enable the matching classification plugins. Without its key, a provider runs a reduced keyless lookup, skips the atom, or records an error for itself. In every case, the run continues and the other providers' artifacts are stored. `example.env` also lists `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and `GOOGLE_GENERATIVE_AI_API_KEY`. Classification and enrichment do not use them. ## Enable a provider 1. Add the key to `.env` in the root of the repository. 2. Restart the services that read it. Docker Compose recreates any container whose configuration changed. ```bash docker compose up -d atom-services workers-classification workers-enrichment ``` 3. Send atom services an input from the provider's domain: ```bash curl -X POST http://localhost:4010/v1/process \ -H 'Content-Type: application/json' \ -d '{"rawInput":"https://open.spotify.com/track/"}' ``` Replace `` with the identifier from any Spotify track URL. The provider's artifact appears in the response's `enrichment.artifacts`. If the key is missing or rejected, the provider appears in `enrichment.skipped` or `enrichment.errors` instead. ## Presets A preset is a named set of enrichment plugins. The workers use `WORKERS_DEFAULT_PRESET` and atom services use `ATOM_SERVICES_DEFAULT_PRESET`. Both default to `default`. A request to atom services can also choose a preset in its `enrichment.preset` field. | Preset | Plugins | | ---------- | ------------------------------------------------ | | `default` | Every provider on this page | | `company` | OpenGraph, Brand, and Favicon | | `music` | OpenGraph, Spotify, MusicBrainz, and Apple Music | | `crypto` | Etherscan and CoinGecko | | `academic` | Crossref and Wikipedia | | `custom` | The same plugins as `default` | ## Share a cache between replicas Each worker and atom services instance caches enrichment results in memory. To share one cache between replicas, use Upstash Redis: ```bash WORKERS_CACHE_PROVIDER=upstash ATOM_SERVICES_CACHE_PROVIDER=upstash UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= ``` Replace `` and `` with the REST credentials of an Upstash Redis database. If the URL or the token is missing, the service falls back to the in-memory cache and logs a warning. Set a cache provider to `none` to disable caching. > [!NOTE] > The stock `docker-compose.yml` passes the Upstash credentials to the > services, but not the `*_CACHE_PROVIDER` or `*_DEFAULT_PRESET` variables. > To set those under Docker Compose, add them to the services' `environment` > in a `docker-compose.override.yml` file, which Docker Compose reads > automatically. ## Add a source See [Enrichment plugins](https://learn.intuition.box/docs/extend/enrichment-plugins). ## Related - [Configuration](https://learn.intuition.box/docs/node/configuration) - [Atom services](https://learn.intuition.box/docs/build/atom-services) - [Indexing scope](https://learn.intuition.box/docs/extend/indexing-scope) --- # Indexing scope A node does not have to index and process everything. Indexing scope is the set of choices that decide what a node handles. The choices apply at four separate layers, and they differ in whether you can undo them. ## The four layers | Layer | Chooses | Set with | Discards source data | | ---------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------- | | Ingestion | The chain, contract, block range, and event types the indexer stores | `CHAIN_ID`, the `MULTIVAULT_*` variables, and the event list in the indexer's manifest | Yes. Events outside the scope are never stored | | Projection | Which read models are built from the stored events | `ENABLED_PROJECTIONS` or `DISABLED_PROJECTIONS` | No. The event store remains complete | | Processing | Which atoms are enriched | `WORKERS_PROCESSING_SCOPE` and the provider keys | No. Atoms remain, but their artifacts may be partial | | Read | What an API or application exposes | Request filters and application logic | No | Keep the layers distinct. A node that serves only music data still needs the chain events that establish atom identities, while it can skip market projections and unrelated enrichment providers. > [!IMPORTANT] > Only ingestion scope is irreversible without re-indexing. Narrow it only > when you are certain the node will never need the excluded events. Use the > other layers to reduce cost while keeping the option to widen later. ## Ingestion The chain variables set the network, the `MultiVault` contract, and the block range. See [Index the chain](https://learn.intuition.box/docs/node/index-the-chain). The event types are listed in the indexer's manifest, `crates/rindexer-ingestion/rindexer.yaml`. By default, it ingests every `MultiVault` event that Core uses. ## Projections Projections run by name. Set one of these variables, as a comma-separated list, to choose which ones run: - `ENABLED_PROJECTIONS` runs only the projections it lists. - `DISABLED_PROJECTIONS` runs every projection except the ones it lists. It is ignored when `ENABLED_PROJECTIONS` is set. By default, Docker Compose disables `funnel_tracker`, `user_activity_batch`, `vault_state:dual`, and `vault_holders_index:dual`. If you use `ENABLED_PROJECTIONS`, include `atom_context:dual` to keep recording the context URIs that atoms register onchain. Each projection needs certain events. For example, `vault_state` needs `Deposited`, `Redeemed`, and `SharePriceChanged`, and `core_entities` needs `AtomCreated` and `TripleCreated`. The dry run below checks these requirements for you. ## Processing Set `WORKERS_PROCESSING_SCOPE` to `full` (the default), `music`, `podcasts`, or `music-and-podcasts`. - Parsing and classification still run for every atom, so atom identity and classification evidence are preserved. - Enrichment runs only for atoms in the selected domains, and only with the providers for those domains. The worker marks every other atom's enrichment as `skipped`. - Changing the scope does not re-enrich atoms that were skipped earlier, and does not delete artifacts that are outside the new scope. Domain filters such as music apply only at the processing layer. They cannot be applied at ingestion, because a term's domain is known only after its atom has been parsed and classified. ## Financial events Deposits, redemptions, and share price changes drive the market read models: vaults, positions, signals, and leaderboards. If a node does not ingest these events, those read models are incomplete. Treat their values as unavailable, not as zero balances. The dry run reports which read models a configuration can build, and rejects a configuration that selects a market projection without the events it needs. ## Validate a configuration Describe a scope in a JSON file, and check it before you start indexing. The check prints what the configuration would do. It does not change any service. ### Prerequisites - A clone of the Intuition Core repository, with its dependencies installed (`bun install`). ### Steps 1. Create a scope file, such as `my-scope.json`: ```json { "scope": { "preset": "music-and-podcasts", "ingestion": { "chain_id": 13579, "rpc_url": "https://testnet.rpc.intuition.systems/http", "contract": "0xeBc49d356B7f64D888130D85CC6D17114a6843ec", "start_block": 9030416, "end_block": 9030916 }, "projections": { "exclude": [] }, "processing": { "providers": { "include": ["opengraph", "jsonld", "spotify", "podcast-index"], "require_keys": false } } } } ``` 2. Run the dry run from the root of the repository: ```bash make scope-dry-run SCOPE_CONFIG=./my-scope.json ``` Without `SCOPE_CONFIG`, the command checks the bundled example, `docs/indexing-scope.example.json`. The command prints JSON that contains the equivalent chain variables, the events and projections the scope selects, each projection's status (`available` or `unavailable`) with the events it requires, and a preview of the indexer's manifest. It redacts credentials in the RPC URL. To apply the scope, set the printed chain variables, set `ENABLED_PROJECTIONS` to the selected projections, and set `WORKERS_PROCESSING_SCOPE`. If you narrowed the events, edit the manifest's event list to match. ### Fields | Field | Meaning | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `preset` | A preset from the table below. It fills in any layer that you do not list explicitly. Default `full` | | `ingestion.chain_id` | The chain identifier. A positive integer | | `ingestion.rpc_url` | The chain's RPC endpoint | | `ingestion.contract` | The `MultiVault` address | | `ingestion.start_block`, `ingestion.end_block` | The block range. `end_block` is optional and must not be lower than `start_block` | | `ingestion.events.include`, `ingestion.events.exclude` | Event names: `AtomCreated`, `TripleCreated`, `Deposited`, `Redeemed`, `SharePriceChanged`, or `ProtocolFeeAccrued`. The two lists cannot overlap | | `projections.bundle` | A preset whose projections to use. Defaults to `preset` | | `projections.include`, `projections.exclude` | Projection names, such as `core_entities` or `vault_state` | | `processing.classifications.include` | Domains to process: `music` or `podcast` | | `processing.providers.include` | Providers for those domains: `opengraph`, `jsonld`, `spotify`, `podcast-index`, `wikipedia`, `wikidata`, `musicbrainz`, or `apple-music` | | `processing.providers.require_keys` | Whether the selected providers must have their keys. Default `false`, so a provider without its key degrades gracefully | ## Presets A preset is a named combination of choices across the layers. | Preset | Events | Projections | Processing | | ----------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------- | | `full` | All | All, except the `:dual` variants | Every atom | | `kg-only` | `AtomCreated`, `TripleCreated` | `event_log`, `account_registry`, and `core_entities` | Every atom | | `market-only` | `Deposited`, `Redeemed`, `SharePriceChanged`, `ProtocolFeeAccrued` | Vault, position, holder, signal, and leaderboard projections | Every atom | | `no-analytics` | All | All except signal analytics, leaderboards, and product analytics | Every atom | | `music`, `podcasts`, `music-and-podcasts` | `AtomCreated`, `TripleCreated` | As `kg-only` | Only atoms in the named domains | ## Related - [Index the chain](https://learn.intuition.box/docs/node/index-the-chain) - [Configuration](https://learn.intuition.box/docs/node/configuration) - [Enrichment providers](https://learn.intuition.box/docs/extend/enrichment-providers) --- # How Intuition works Intuition has three layers. You can use each one on its own. | Layer | What it is | Where it runs | | -------------- | -------------------------------------------------------------------------- | --------------------- | | Protocol | Smart contracts that register atoms and triples and hold a vault for each | The Intuition network | | Packages | TypeScript libraries for identifiers, vocabulary, and contract interaction | Your application | | Intuition Core | An indexer, a processing pipeline, and a query API | Your infrastructure | ## The protocol The protocol's central contract is `MultiVault`. It does two things. It **registers terms**. A term is an atom or a triple. Registration records that the term exists and who created it. A term can be registered only once, because its identifier is derived from its content. It **holds a vault for every term**. Anyone can deposit TRUST, the network's native currency, into a term's vault and receive shares. The amount deposited on a term is a public measure of how much its supporters stand behind it. See [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals). The protocol runs on the Intuition network, which has a mainnet and a testnet. Chain IDs, RPC endpoints, and contract addresses are listed in [Networks and contracts](https://learn.intuition.box/docs/build/networks-and-contracts). ## The packages The `@0xintuition` packages are ESM-only TypeScript libraries published on npm. They let an application work with Intuition data without running any service: - `@0xintuition/ids` derives identifiers. - `@0xintuition/predicates` and `@0xintuition/classifications` supply the shared vocabulary, so that applications describe the same things in the same way. - `@0xintuition/protocol` and `@0xintuition/deployments` supply contract ABIs, interaction helpers, chain definitions, and addresses. See [Packages](https://learn.intuition.box/docs/build/packages) for the full list. ## Intuition Core You can read individual terms directly from the chain. Answering questions such as "which triples mention this atom" or "what is this URL" needs an index. Intuition Core is that index, and it is open source. It contains: - an **indexer** that decodes `MultiVault` events into an event store, - **projections** that turn those events into read models and a knowledge graph, - **workers** that parse, classify, and enrich every atom, - a **REST API** that serves the result, - **atom services**, a stateless API that classifies and enriches any input without storing it, and - an **explorer**, a web dashboard for inspecting your node. Anyone can run Core and rebuild the graph from chain events. You do not need anyone's hosted service to read Intuition data. See [Run a node](https://learn.intuition.box/docs/node/run-a-node). ## Local first, onchain when it matters An identifier depends only on content, so a term does not have to be registered to be useful. A typical application works like this: 1. It derives identifiers locally and stores atoms and triples in its own node. This costs nothing and needs no transaction. 2. It reads the chain to learn which of those terms are already registered. 3. It registers the terms that should be public and permanent. A triple can be registered only after its subject, predicate, and object are. Registering a term does not rename it. The identifier is the same before and after, so data that different people create locally converges on the same records once it is published. ## Next steps - [Atoms](https://learn.intuition.box/docs/concepts/atoms) and [Triples](https://learn.intuition.box/docs/concepts/triples) define the data model. - [Architecture](https://learn.intuition.box/docs/reference/architecture) describes Core's pipeline in detail. --- # Quickstart In this quickstart, you derive identifiers on your own machine and then ask the Intuition mainnet whether one of them is registered. You need no wallet, no API key, and no account. It takes about five minutes. ## Before you begin You need: - Node.js 22.18 or later. These versions run TypeScript files directly. - pnpm. Any package manager works, but the commands below use pnpm. ## Set up a project 1. Create a folder for the project and open it: ```bash mkdir intuition-quickstart cd intuition-quickstart ``` 2. Create a file named `package.json` with this content. The `@0xintuition` packages are ESM-only, so the project must use ES modules. ```json { "type": "module" } ``` 3. Install the packages: ```bash pnpm add @0xintuition/ids@alpha @0xintuition/deployments@alpha @0xintuition/protocol@3.0.0 viem ``` | Package | Purpose in this guide | | -------------------------- | ---------------------------------------------------- | | `@0xintuition/ids` | Derives atom and triple identifiers | | `@0xintuition/deployments` | Supplies the chain definition and contract addresses | | `@0xintuition/protocol` | Supplies the `MultiVault` contract's ABI | | `viem` | Reads from the chain | ## Derive an atom identifier An [atom](https://learn.intuition.box/docs/concepts/atoms) identifies one thing. It is defined by its data, such as a URL, a word, or a JSON document, and its identifier is a hash of that data. 1. Create a file named `quickstart.ts` with this code: ```ts import { calculateAtomId } from '@0xintuition/ids'; const atomId = calculateAtomId('hello'); console.log(atomId); ``` 2. Run it: ```bash node quickstart.ts ``` The script prints the atom's identifier: ```text 0xa0e157e5fa1b17d3b54ec73622ce3317296920a06502661617613d59f58e947e ``` If Node.js also prints a warning that type stripping is experimental, you can ignore it. The result depends only on the input. Run the script again, on any machine, and you get the same value. Change one character of the input, and the whole identifier changes. ## Link atoms into a triple A [triple](https://learn.intuition.box/docs/concepts/triples) is a claim that links a subject, a predicate, and an object. Here all three are atoms, and the triple's identifier is derived from their three identifiers. 1. Replace the contents of `quickstart.ts` with this code: ```ts import { calculateAtomId, calculateTripleId } from '@0xintuition/ids'; const subjectId = calculateAtomId('hello'); const predicateId = calculateAtomId('is a'); const objectId = calculateAtomId('greeting'); const tripleId = calculateTripleId(subjectId, predicateId, objectId); console.log(tripleId); ``` 2. Run `node quickstart.ts` again. The script prints the triple's identifier: ```text 0xb9efcf81eb4858599ed97a6f69a30462f0720a210e9e4947fe4793d3545dc43c ``` > [!NOTE] > This example uses a plain string as the predicate to keep it short. > Applications use the canonical predicates, which have fixed identifiers. See > [Predicates](https://learn.intuition.box/docs/concepts/predicates). ## Check whether a term is registered Atoms and triples are both called **terms**. The `MultiVault` contract registers terms onchain and reports whether a given term exists. 1. Replace the contents of `quickstart.ts` with this code: ```ts import { getMultiVaultAddressFromChainId, intuitionMainnet, } from '@0xintuition/deployments'; import { calculateAtomId } from '@0xintuition/ids'; import { MultiVaultAbi } from '@0xintuition/protocol'; import { createPublicClient, http } from 'viem'; const client = createPublicClient({ chain: intuitionMainnet, transport: http(), }); const isCreated = await client.readContract({ address: getMultiVaultAddressFromChainId(intuitionMainnet.id), abi: MultiVaultAbi, functionName: 'isTermCreated', args: [calculateAtomId('hello')], }); console.log(isCreated); ``` `http()` with no argument uses the public RPC endpoint from the chain definition, so you need no API key. 2. Run `node quickstart.ts` again. The script prints: ```text true ``` The atom for `hello` is already registered on mainnet. Someone else created it. You derived the same identifier without contacting them or any server, which is the property the rest of Intuition is built on. ## What you learned - An identifier is a pure function of content, so you can derive it offline. - The identifier you derive is the identifier the protocol uses onchain. - Reading the chain needs only the public RPC endpoint. ## Next steps - [How Intuition works](https://learn.intuition.box/docs/get-started/how-it-works) explains the parts of the system. - [Identifiers](https://learn.intuition.box/docs/concepts/identifiers) describes the derivation in detail. - [Run a node](https://learn.intuition.box/docs/node/run-a-node) gives you a queryable graph of your own. - [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain) registers terms with the protocol. --- # Configuration Intuition Core reads its settings from environment variables. When you run your node with Docker Compose, you set them in `.env` at the root of the repository. The bootstrap script creates `.env` from `example.env`, which holds the defaults for local use. ## How Compose uses `.env` Docker Compose reads `.env` and passes a defined set of variables to each container. It sets the database connection strings and the ports inside the containers itself. Under Compose, a variable takes effect only if it appears in [Settings in `.env`](https://learn.intuition.box/docs/node/configuration#settings-in-env). After you change `.env`, apply the change: ```bash docker compose up -d ``` Add `--profile indexing` if your node indexes the chain. Compose recreates only the containers whose settings changed. > [!IMPORTANT] > `.env` holds your provider keys. Keep it out of version control, and > restrict who can read it with `chmod 600 .env`. ## Settings in `.env` ### Query API | Variable | Default | Purpose | | --------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------- | | `API_AUTH` | `public-read` | `open`, `public-read`, or `gated`. See [Write data](https://learn.intuition.box/docs/node/write-data#authentication-modes) | | `API_ALLOWED_ORIGINS` | Empty, which allows every origin | Comma-separated origins allowed by CORS | | `API_RATE_LIMIT_RPM` | `120` | Requests per minute for each API key, or for each IP address when no key is sent. `0` means no limit | ### Chain and indexer These apply when you run the `indexing` profile. See [Index the chain](https://learn.intuition.box/docs/node/index-the-chain). | Variable | Required | Purpose | | ----------------------------- | -------- | ----------------------------------------------------------------- | | `INTUITION_RPC_URL` | Yes | The chain's RPC endpoint | | `CHAIN_ID` | Yes | The chain's identifier | | `MULTIVAULT_CONTRACT_ADDRESS` | Yes | The `MultiVault` deployment to index | | `MULTIVAULT_START_BLOCK` | Yes | The first block to index | | `MULTIVAULT_END_BLOCK` | No | The last block to index. Empty means follow the head of the chain | ### Projections | Variable | Default | Purpose | | ---------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------- | | `USE_TYPED_READER` | `true` | Read events from the typed event tables. Keep it `true` | | `DISABLED_PROJECTIONS` | `funnel_tracker,user_activity_batch,vault_state:dual,vault_holders_index:dual` | Comma-separated projections to skip | ### Workers | Variable | Default | Purpose | | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------- | | `WORKERS_PROCESSING_SCOPE` | `full` | `full`, `music`, `podcasts`, or `music-and-podcasts`. See [Indexing scope](https://learn.intuition.box/docs/extend/indexing-scope) | ### Host ports These set the ports on your machine. The ports inside the containers do not change. | Variable | Default | Service | | ------------------------- | ------- | --------------- | | `API_HOST_PORT` | `3000` | `api` | | `ATOM_SERVICES_HOST_PORT` | `4010` | `atom-services` | | `POSTGRES_KG_HOST_PORT` | `5432` | `postgres-kg` | | `TIMESCALE_HOST_PORT` | `5433` | `timescale` | | `REDIS_HOST_PORT` | `6379` | `redis` | The worker ports (4110 to 4112), the indexer's port (9091), the projections' port (9092), and Anvil's port (8545) are fixed in `docker-compose.yml`. If you change `API_HOST_PORT`, point your clients and the [explorer](https://learn.intuition.box/docs/node/explorer#configuration) at the new port. If you change `POSTGRES_KG_HOST_PORT`, `make keys` no longer reaches the database; use the key script with your own connection string, as described in [Manage keys](https://learn.intuition.box/docs/node/write-data#manage-keys). ### Provider keys Every provider key is optional. Without a key, the corresponding plugin is skipped and the rest of the pipeline continues. See [Enrichment providers](https://learn.intuition.box/docs/extend/enrichment-providers) for the full list and for how to obtain each key. Compose also passes the Upstash credentials, `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`. They take effect only when you also set the cache provider to `upstash`, which needs an override file, as described in [Settings for running a service yourself](https://learn.intuition.box/docs/node/configuration#settings-for-running-a-service-yourself). > [!NOTE] > `example.env` also contains rollout switches that are off by default: > `API_ATOM_SEMANTIC_READS_ENABLED`, `VITE_ATOM_SEMANTIC_READS_ENABLED`, > `WORKERS_IID_READ_ENABLED`, and `WORKERS_IID_RESOLUTION_ENABLED`. Leave > them set to `false`. ## Settings for running a service yourself When you run a service image directly, or a service from source, set its variables yourself. Under Compose, these variables keep the values that `docker-compose.yml` sets, or their defaults, whatever `.env` contains. To change one of them under Compose, add it to the service's `environment` in a `docker-compose.override.yml` file at the root of the repository. Compose reads that file automatically. For example: ```yaml services: workers-enrichment: environment: WORKERS_CONCURRENCY: '8' ``` If you set `COMPOSE_FILE`, as the [published-image mode](https://learn.intuition.box/docs/node/container-images) does, add the override file to it: `COMPOSE_FILE=docker-compose.yml:docker-compose.published.yml:docker-compose.override.yml`. ### Datastores | Variable | Used by | Purpose | | ----------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `DATABASE_KG_URL` | API, workers, KG migration, projections | The knowledge graph. Locally: `postgresql://intuition:intuition@localhost:5432/intuition_kg` | | `DATABASE_URL` | Indexer, projections, event store migration | The event store. Locally: `postgresql://intuition:intuition@localhost:5433/intuition_timescale` | ### Query API | Variable | Default | Purpose | | ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `API_PORT` | `3000` | Listening port | | `API_TRUST_PROXY` | Empty | Set to `1` only behind a reverse proxy that overwrites `X-Forwarded-For`. The API then rate-limits requests without a key by that header | ### Workers | Variable | Default | Purpose | | ------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `WORKERS_PORT` | `4110` | The parse worker's health port. The enrichment worker uses it plus 1, classification plus 2 | | `WORKERS_CONCURRENCY` | `4` | Atoms each worker processes at once | | `WORKERS_LEASE_MS` | `60000` | How long a worker holds an atom. An expired lease is reclaimed | | `WORKERS_MAX_ATTEMPTS` | `5` | Attempts per stage | | `WORKERS_PARSE_REMOTE_FETCH` | `true` | Fetch remote content while parsing | | `WORKERS_PARSE_ALLOW_HTTP` | `false` | Allow unencrypted HTTP fetches | | `WORKERS_PARSE_IPFS_GATEWAY_BASE_URL` | Unset | An IPFS gateway for resolving IPFS URIs | | `WORKERS_DEFAULT_PRESET` | `default` | The enrichment preset. See [Presets](https://learn.intuition.box/docs/extend/enrichment-providers#presets) | | `WORKERS_CACHE_PROVIDER` | `memory` | `memory`, `none`, or `upstash`. See [Share a cache between replicas](https://learn.intuition.box/docs/extend/enrichment-providers#share-a-cache-between-replicas) | ### Atom services | Variable | Default | Purpose | | ------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `ATOM_SERVICES_PORT` | `4010` | Listening port | | `ATOM_SERVICES_AUTH_TOKEN` | Unset | When set, requests must present this bearer token | | `ATOM_SERVICES_DEFAULT_PRESET` | `default` | The enrichment preset. See [Presets](https://learn.intuition.box/docs/extend/enrichment-providers#presets) | | `ATOM_SERVICES_CACHE_PROVIDER` | `memory` | `memory`, `none`, or `upstash`. See [Share a cache between replicas](https://learn.intuition.box/docs/extend/enrichment-providers#share-a-cache-between-replicas) | ### Indexer and projections | Variable | Default | Purpose | | ------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `METRICS_PORT` | `9091` | The indexer's metrics and health port | | `PROJECTIONS_METRICS_PORT` | `9092` | The projections' metrics and health port | | `SURREAL_DB_URL` | `ws://localhost:8000` | Set it to an empty value. Core keeps graph data in PostgreSQL only. With the default, the projections try to reach SurrealDB and fail | | `USE_TYPED_READER` | `false` | Set it to `true`, as Compose does | | `ENABLED_PROJECTIONS` | Empty | Comma-separated projections to run. When set, only these run | | `DISABLED_PROJECTIONS` | Empty | Comma-separated projections to skip. Use the Compose default shown in [Projections](https://learn.intuition.box/docs/node/configuration#projections) | | `PROJECTIONS_BATCH_SIZE` | `500` | Events per batch | | `PROJECTIONS_POLL_INTERVAL_MS` | `1000` | Polling interval | ## Related - [Index the chain](https://learn.intuition.box/docs/node/index-the-chain) - [Operate a node](https://learn.intuition.box/docs/node/operate-a-node) - [Troubleshooting](https://learn.intuition.box/docs/node/troubleshooting) --- # Container images By default, Docker Compose builds each service from the repository. You can instead run the images published to the GitHub Container Registry (GHCR), which skips the build. ## Published images | Image | Role | | --------------------------------------------------------- | --------------------------------------------------------------------- | | `ghcr.io/0xintuition/intuition-core-api` | The query API. Compose also uses it for the knowledge graph migration | | `ghcr.io/0xintuition/intuition-core-atom-services` | Classification and enrichment over HTTP | | `ghcr.io/0xintuition/intuition-core-workers` | The parse, classification, and enrichment workers | | `ghcr.io/0xintuition/intuition-core-rindexer-ingestion` | The indexer | | `ghcr.io/0xintuition/intuition-core-projections` | The projections | | `ghcr.io/0xintuition/intuition-core-timescale-migrations` | The event store migration | Each image is built for `linux/amd64` and `linux/arm64`. The stack also uses `timescale/timescaledb-ha:pg17` for both databases and `redis:7-alpine`. The devnet deployer is not published; Compose always builds it from source. ## Choose a release Images are tagged with release versions, such as `v0.1.0-rc.2`. Each published build also has an immutable `sha-` tag followed by the first 12 characters of its commit hash. Release candidates never receive the `latest` tag, and Compose has no fallback to it, so always name a version. Each release's notes, in the repository's `docs/releases` folder, list the version and the digest of every image. At the time of writing, the current release is the release candidate `v0.1.0-rc.2`. It is intended for trials, not production. ## Run a tagged release Published-image mode requires a current version of Docker Compose v2. The legacy `docker-compose` v1 command does not work. 1. Create `.env`, if you have not already: ```bash cp example.env .env ``` 2. Start the node from the published images. Replace `vX.Y.Z` with a release version: ```bash make up-published IMAGE_TAG=vX.Y.Z ``` To index the chain as well, run `make index-published IMAGE_TAG=vX.Y.Z`. 3. Optionally, run the smoke test against the same images: ```bash make smoke-published IMAGE_TAG=vX.Y.Z ``` The equivalent Compose command is: ```bash COMPOSE_FILE=docker-compose.yml:docker-compose.published.yml \ INTUITION_CORE_IMAGE_TAG=vX.Y.Z \ docker compose up -d ``` ## Pin by digest in production A tag can be moved. A digest cannot. For production, and when you reproduce an incident, pin every service to a digest. Replace each `sha256:...` with the digest from the release notes: ```bash export INTUITION_CORE_API_IMAGE=ghcr.io/0xintuition/intuition-core-api@sha256:... export INTUITION_CORE_ATOM_SERVICES_IMAGE=ghcr.io/0xintuition/intuition-core-atom-services@sha256:... export INTUITION_CORE_WORKERS_IMAGE=ghcr.io/0xintuition/intuition-core-workers@sha256:... export INTUITION_CORE_RINDEXER_INGESTION_IMAGE=ghcr.io/0xintuition/intuition-core-rindexer-ingestion@sha256:... export INTUITION_CORE_PROJECTIONS_IMAGE=ghcr.io/0xintuition/intuition-core-projections@sha256:... export INTUITION_CORE_TIMESCALE_MIGRATIONS_IMAGE=ghcr.io/0xintuition/intuition-core-timescale-migrations@sha256:... export COMPOSE_FILE=docker-compose.yml:docker-compose.published.yml docker compose config -q docker compose up -d ``` `docker compose config -q` checks the configuration and prints nothing when it is valid. Pin all six images from the same release together. ## Verify an image's provenance Each published image carries a software bill of materials and a build provenance attestation. Before you deploy an image to an environment that matters, verify it with the [GitHub CLI](https://cli.github.com/): ```bash gh attestation verify \ oci://ghcr.io/0xintuition/intuition-core-api@sha256:... \ --bundle-from-oci \ --repo 0xIntuition/intuition-core \ --signer-workflow github.com/0xIntuition/intuition-core/.github/workflows/publish-images.yml ``` Repeat the command for each image you deploy. ## Run an image directly Compose supplies every setting below. They matter when you run an image by itself, for example on Kubernetes. [Configuration](https://learn.intuition.box/docs/node/configuration#settings-for-running-a-service-yourself) describes each variable. | Image | Required settings | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | API | `DATABASE_KG_URL` | | Workers | `DATABASE_KG_URL`, and the worker to run as the command: `kg-parse-worker`, `kg-classification-worker`, or `kg-enrichment-worker` | | Atom services | None | | Indexer | `DATABASE_URL`, `INTUITION_RPC_URL`, `CHAIN_ID`, `MULTIVAULT_CONTRACT_ADDRESS`, `MULTIVAULT_START_BLOCK` | | Projections | `DATABASE_URL`, `DATABASE_KG_URL` to write atoms and triples, an empty `SURREAL_DB_URL`, and `USE_TYPED_READER=true` | | Event store migration | `DATABASE_URL` | Run both migrations to completion before you start the other services, as Compose does. The knowledge graph migration runs from the API image, in `/repo/packages/database-kg`, with the command `bun src/migrate.ts`. ## Related - [Configuration](https://learn.intuition.box/docs/node/configuration) - [Operate a node](https://learn.intuition.box/docs/node/operate-a-node#upgrade-the-node) - [Run a node](https://learn.intuition.box/docs/node/run-a-node) --- # Explorer Intuition Core includes a web application for inspecting a node. It reads everything through the same REST API that your own application would use. ## Before you begin - A running node. See [Run a node](https://learn.intuition.box/docs/node/run-a-node). - The repository's dependencies installed. The bootstrap script does this. If you skipped it, run `bun install --frozen-lockfile` from the repository root. ## Start the explorer ```bash cd apps/explorer bun run dev ``` Open [http://localhost:3100](http://localhost:3100/) in your browser. The explorer expects the query API at `http://localhost:3000`. ## What it shows | View | Contents | | ---------- | ----------------------------------------------------------------------------------------------------------------- | | Dashboard | Counts of atoms, triples, predicates, and accounts, pipeline status by stage, service health, and recent activity | | Atoms | A searchable table. Each atom's page shows its data, classification, enrichment artifacts, triples, and events | | Triples | Triples shown as subject, predicate, and object, with readable labels | | Predicates | The node's predicate registry | | Events | The append-only activity log | | Schema | The live table definitions from `GET /api/schema` | | Playground | Forms that create atoms and triples, each showing the equivalent `curl` command | The dashboard's service health grid checks the API, atom services, the three workers, the indexer, and the projections. The indexer and the projections show as down unless the [indexing profile](https://learn.intuition.box/docs/node/index-the-chain) is running. ## Configuration | Variable | Default | Purpose | | ------------------------- | --------------------------------------- | ---------------------------------------------------------------------- | | `VITE_API_URL` | `http://localhost:3000` | The query API to read from | | `EXPLORER_STATUS_TARGETS` | The health endpoints on the local ports | The services the dashboard checks, as comma-separated `name=url` pairs | Set these when you start the explorer. For example, if your API listens on port 3001: ```bash VITE_API_URL=http://localhost:3001 bun run dev ``` The explorer checks service health from its own server, not from your browser, so the health endpoints need no CORS configuration. If you restrict `API_ALLOWED_ORIGINS` on the API, include `http://localhost:3100`, or the explorer cannot read from it. ## Use it as a reference The explorer's API client, `apps/explorer/src/lib/api.ts`, is a single file of small typed functions over `fetch`. It is a practical starting point for your own client. ## Related - [REST API](https://learn.intuition.box/docs/build/rest-api) - [Write data](https://learn.intuition.box/docs/node/write-data) - [Operate a node](https://learn.intuition.box/docs/node/operate-a-node#check-health) --- # Index the chain Indexing fills your node with the atoms and triples registered onchain. It uses the `indexing` Compose profile, a group of services that starts only when you name it. The profile adds two services: - The **indexer** reads `MultiVault` events from the chain and writes them to the [event store](https://learn.intuition.box/docs/reference/glossary). - The **projections** turn those events into read models, and write the resulting atoms and triples into the knowledge graph. The Intuition testnet's RPC endpoint is public and needs no key. ## Before you begin - A running node. See [Run a node](https://learn.intuition.box/docs/node/run-a-node). - Local ports 9091 and 9092 free. The indexer and the projections publish their metrics and health checks on them. ## Configure the chain Set the following in `.env`. These values index a 500-block window of the Intuition testnet, which is a quick way to confirm your setup works. ```bash INTUITION_RPC_URL=https://testnet.rpc.intuition.systems/http CHAIN_ID=13579 MULTIVAULT_CONTRACT_ADDRESS=0xeBc49d356B7f64D888130D85CC6D17114a6843ec MULTIVAULT_START_BLOCK=9030416 MULTIVAULT_END_BLOCK=9030916 ``` | Variable | Required | Meaning | | ----------------------------- | -------- | -------------------------------------------------------------------------- | | `INTUITION_RPC_URL` | Yes | The chain's RPC endpoint | | `CHAIN_ID` | Yes | The chain's identifier | | `MULTIVAULT_CONTRACT_ADDRESS` | Yes | The `MultiVault` deployment to index | | `MULTIVAULT_START_BLOCK` | Yes | The first block to index. Use the block in which `MultiVault` was deployed | | `MULTIVAULT_END_BLOCK` | No | The last block to index. Leave it empty to follow the chain continuously | To index another deployment, such as mainnet, take its chain ID, RPC endpoint, and `MultiVault` address from [Networks and contracts](https://learn.intuition.box/docs/build/networks-and-contracts). ## Start indexing 1. Start the node with the indexing profile: ```bash docker compose --profile indexing up -d ``` `make index` runs the same command. On a machine where the node is not set up yet, `scripts/bootstrap.sh --indexing` (or `make bootstrap-index`) sets it up and starts indexing in one step. 2. Watch the indexer: ```bash docker compose --profile indexing logs -f indexer ``` 3. When the range is indexed, the indexer exits with code 0. Confirm it: ```bash docker compose --profile indexing ps -a indexer ``` The status reads `Exited (0)`. Exiting at the end of a bounded range is expected. Any other exit code is a failure. 4. Check that the atoms reached the knowledge graph: ```bash curl http://localhost:3000/api/stats ``` On a new node, the 500-block window above produces 201 events and 50 atoms. The projections can take a few seconds to catch up after the indexer exits. ## Follow the chain To index the full history and keep up with new blocks: 1. In `.env`, leave the end block empty: ```bash MULTIVAULT_END_BLOCK= ``` 2. Start the profile again: ```bash docker compose --profile indexing up -d ``` The indexer then syncs to the head of the chain and keeps running. Before a full sync, make sure you have enough disk space and RPC capacity for the chain's complete history. Both grow with the chain, so there is no fixed figure. ## What happens to the data 1. The indexer writes each event to the `event_store` table and to a typed table for its event type. 2. Projections read the event store and maintain read models for vaults, positions, share prices, and signals. 3. The `core_entities` projection writes atoms and triples into the knowledge graph. 4. The same workers that process API writes parse, classify, and enrich the indexed atoms. Indexed terms and terms written through the API share one graph. A term you created locally and that is later registered onchain is recognized as the same term, because its identifier is the same. ## Reliability - Each projection records a checkpoint. After a restart, it resumes where it stopped. - The event store ignores an event it already holds, identified by its transaction hash and log index, so indexing a block again does not duplicate data. - The indexer stays 10 blocks behind the head of the chain, so a chain reorganization shallower than 10 blocks does not reach the event store. ## Monitor progress ```bash curl -s http://localhost:9091/metrics | grep intuition_sync_progress_percent curl http://localhost:9092/health/ready curl http://localhost:3000/api/stats ``` The first command shows the indexer's progress, from 0 to 100, for each event type. The second returns `OK` once the projections are processing events, and `503` before then. The third shows the counts in the knowledge graph. ## Related - [Local devnet](https://learn.intuition.box/docs/node/local-devnet) runs a chain of your own. - [Indexing scope](https://learn.intuition.box/docs/extend/indexing-scope) limits what a node ingests and processes. - [Configuration](https://learn.intuition.box/docs/node/configuration) lists every setting. --- # Local devnet The `devnet` Compose profile runs the whole system on your machine: a local [Anvil](https://getfoundry.sh/) chain, the Intuition contracts deployed to it, and your node indexing that chain. After the first image download, it works offline. The contracts come from the published [`@0xintuition/contracts-v2`](https://www.npmjs.com/package/@0xintuition/contracts-v2) package. Core V2 pins its version, 1.1.0-alpha.0, so the devnet runs that version of the contracts. ## Before you begin - A running node. See [Run a node](https://learn.intuition.box/docs/node/run-a-node). - Local port 8545 free, for the chain's RPC endpoint. - To create atoms onchain from your terminal, also install [Foundry](https://getfoundry.sh/), which provides `cast`, and [jq](https://jqlang.github.io/jq/). ## Start the chain and deploy the contracts 1. Start Anvil and run the deployment job: ```bash docker compose --profile devnet up -d anvil devnet-deploy ``` `make devnet` runs the same command. 2. Follow the deployment: ```bash docker compose logs -f devnet-deploy ``` The job deploys the full system, creates a test atom to confirm that events are emitted, and writes the contract addresses to `devnet/deployments-devnet.json`. It ends by printing the `.env` values that point your indexer at the new chain. Running the job again detects the existing deployment and verifies it instead of deploying a second copy. ## Point the indexer at your chain 1. Read the `MultiVault` address and its deployment block from the deployments file: ```bash jq -r '.MultiVault, .deployBlock' devnet/deployments-devnet.json ``` 2. Set the following in `.env`, using those two values: ```bash INTUITION_RPC_URL=http://anvil:8545 CHAIN_ID=31337 MULTIVAULT_CONTRACT_ADDRESS=0x... MULTIVAULT_START_BLOCK=... MULTIVAULT_END_BLOCK= USE_TYPED_READER=true ``` `anvil` is the chain's name on the Compose network, so this RPC address works for the containers. From your own terminal, the chain is at `http://localhost:8545`. 3. Start the chain and the indexing profile together: ```bash docker compose --profile devnet --profile indexing up -d ``` Treat `devnet/deployments-devnet.json` as the source of truth. Chain state persists in the `anvil_data` volume across restarts, so the address and the deployment block can differ from those of a fresh deployment. To stop everything, including the chain, and keep its state: ```bash docker compose --profile devnet --profile indexing down ``` ## Create an atom onchain Anvil's first account is funded. Its private key is Foundry's standard development key: it is public, and you must only ever use it on a local chain. ```bash MV=$(jq -r .MultiVault devnet/deployments-devnet.json) COST=$(cast call $MV "getAtomCost()(uint256)" --rpc-url http://localhost:8545 | awk '{print $1}') cast send $MV "createAtoms(bytes[],uint256[])" \ "[$(cast from-utf8 'https://github.com/oven-sh/bun')]" "[$COST]" \ --value $COST \ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ --rpc-url http://localhost:8545 ``` `MV` holds the `MultiVault` address and `COST` holds the minimum amount, in wei, needed to create one atom. Two rules apply to `createAtoms`: - The transaction value must equal the sum of the amounts array, and each amount must be at least `getAtomCost()`. - Atom data must be new. Creating the same data twice reverts with `MultiVault_AtomExists`. Confirm that your node indexed the atom. Within seconds, the atom count rises by one and the new atom appears first in the list: ```bash curl http://localhost:3000/api/stats curl "http://localhost:3000/api/atoms?limit=1" ``` ## Run without Docker With Foundry installed, you can run the chain and the deployer as local processes: ```bash anvil --disable-code-size-limit & bun run devnet:deploy ``` To run the whole stack as local processes, including the chain, install [Process Compose](https://f1bonacc1.github.io/process-compose/installation/), Rust, and `envsubst`, then run: ```bash bun run dev:local -- indexing --devnet ``` For local processes, use `http://localhost:8545` as `INTUITION_RPC_URL`. ## Deploy your own instance to the testnet The same deployer creates a separate protocol deployment on the Intuition testnet (chain 13579), isolated from the canonical one: ```bash PRIVATE_KEY=0x... bun run testnet:deploy ``` Replace `0x...` with the private key of an account you control on the testnet. The account pays gas for about 40 transactions, in testnet TRUST from the faucet at [testnet.hub.intuition.systems](https://testnet.hub.intuition.systems/). The command writes `devnet/deployments-testnet.json` and prints the `.env` values that point your indexer at the new deployment. > [!CAUTION] > Never pass a key that holds mainnet funds. Prefer a fresh account funded > only from the faucet. ## Notes - Anvil must run with `--disable-code-size-limit`, because the `MultiVault` implementation exceeds the default contract size limit. The Compose profile sets this flag for you. - The deployment block is not deterministic. Read it from the deployments file. - The contract version is pinned in `packages/contracts/package.json`. After you change it, run `bun run abis:sync` to regenerate the ABI the indexer reads. ## Related - [Index the chain](https://learn.intuition.box/docs/node/index-the-chain) - [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain) --- # Operate a node This page covers the day-to-day tasks of running a node with Docker Compose. Run every command from the root of the repository. If your node indexes the chain, add `--profile indexing` to each `docker compose` command so that it includes the indexer and the projections. ## Check status ```bash make status ``` This runs `docker compose --profile indexing ps` and lists every service with its state. Long-running services show `running`. The two migration jobs exit when they finish, so `docker compose ps -a` shows them as `Exited (0)`. After a bounded indexing run, the indexer shows `Exited (0)` too. ## Check health Each service answers on its own port. | Service | Endpoint | Healthy response | | ------------------------ | ------------------------------------------------------------------------- | ----------------- | | `api` | `http://localhost:3000/health` | `{"status":"ok"}` | | `atom-services` | `http://localhost:4010/health`, `http://localhost:4010/ready` | `200` | | `workers-parse` | `http://localhost:4110/healthz`, `http://localhost:4110/readyz` | `200` | | `workers-enrichment` | `http://localhost:4111/healthz`, `http://localhost:4111/readyz` | `200` | | `workers-classification` | `http://localhost:4112/healthz`, `http://localhost:4112/readyz` | `200` | | `indexer` | `http://localhost:9091/health` | `OK` | | `projections` | `http://localhost:9092/health/live`, `http://localhost:9092/health/ready` | `OK` | A `health` or `healthz` endpoint reports that the process is up. A `ready` or `readyz` endpoint reports that it can do work, and returns `503` until then. When the API cannot reach its database, `/health` returns `503` with `{"status":"degraded","database":"unreachable"}`. To see how many atoms are in each stage of the pipeline: ```bash curl http://localhost:3000/api/stats/pipeline ``` The indexer and the projections also publish Prometheus metrics at `http://localhost:9091/metrics` and `http://localhost:9092/metrics`. ## Read logs ```bash make logs SERVICE=api ``` This follows the logs of one service. Leave out `SERVICE` to follow every service. The equivalent Compose commands are: ```bash docker compose logs -f api docker compose --profile indexing logs -f --tail=100 indexer projections ``` ## Stop and restart | Task | Command | | ------------------------------------- | ------------ | | Stop every service and keep your data | `make down` | | Start the node again | `make up` | | Start the node with indexing | `make index` | Restarting is safe. The migration jobs run again and skip the migrations already applied, the projections resume from their checkpoints, and the event store never stores the same event twice. ## Data Compose stores data in named volumes. Each name starts with the Compose project name, `intuition-core`. | Volume | Contents | | --------------------------------- | ------------------------------------------------------------ | | `intuition-core_postgres_kg_data` | The knowledge graph: atoms, triples, artifacts, and API keys | | `intuition-core_timescale_data` | The event store and its read models | | `intuition-core_redis_data` | Redis data | | `intuition-core_anvil_data` | The local devnet chain, when you use it | The event store and the indexed part of the knowledge graph can be rebuilt by indexing the chain again. Atoms and triples written through the API, and your API keys, exist only in your knowledge graph. To delete every volume and start from an empty node: ```bash make reset ``` > [!CAUTION] > `make reset` runs `docker compose down -v`, which permanently deletes the > knowledge graph, the event store, and every other volume. Back up first. ## Back up Back up both databases with `pg_dump` while the node is running: ```bash docker compose exec -T postgres-kg pg_dump -U intuition -d intuition_kg -Fc > intuition_kg.dump docker compose exec -T timescale pg_dump -U intuition -d intuition_timescale -Fc > intuition_timescale.dump ``` Each command writes a compressed dump file to the current directory. Store the files somewhere other than the machine that runs the node. Both databases use the TimescaleDB extension. To restore a dump, restore it into the same TimescaleDB version, and follow TimescaleDB's restore procedure: run `SELECT timescaledb_pre_restore();` before `pg_restore`, and `SELECT timescaledb_post_restore();` after it. ## Upgrade the node Back up both databases before every upgrade, and read the release notes in the repository's `docs/releases` folder for changes to configuration or schema. The migration jobs apply new schema changes automatically when the node starts. ### From source ```bash git pull bun install --frozen-lockfile docker compose up -d --build ``` `--build` rebuilds the service images from the new source. Without it, Compose keeps running the images it built before. ### From published images Change the tag or the digests to the new release, then start the node again. Compose pulls the new images before it starts them. ```bash make up-published IMAGE_TAG=vX.Y.Z ``` For digest pins, update every `INTUITION_CORE_*_IMAGE` variable and run `docker compose up -d`. See [Container images](https://learn.intuition.box/docs/node/container-images). ### Roll back To roll back, pin all six images to the digests of the previous release. Changing images does not undo database migrations. If the release you are leaving changed the schema, restore the backup you took before the upgrade. ## Secure the node - Compose publishes every service port on all of the host's network interfaces, and the databases use the default credentials `intuition`/`intuition`. On a machine reachable from a network, block ports 5432, 5433, 6379, 4110 to 4112, 9091, and 9092 from outside access, for example with your cloud provider's firewall. - The worker ports also serve internal debugging state. Never expose them publicly. - Keep `API_AUTH` set to `public-read` or `gated`. Never use `open` on a reachable host. - Keep `.env` out of version control, and restrict it with `chmod 600 .env`. ## Related - [Configuration](https://learn.intuition.box/docs/node/configuration) - [Container images](https://learn.intuition.box/docs/node/container-images) - [Troubleshooting](https://learn.intuition.box/docs/node/troubleshooting) --- # Run a node A [node](https://learn.intuition.box/docs/reference/glossary) is Intuition Core running on your own machine: two databases, a query API, and workers that parse, classify, and enrich every atom. You start it with Docker Compose. It needs no paid account and no API key, and there is no chain to sync before you can use it. ## Requirements - [Docker](https://docs.docker.com/get-docker/) with Docker Compose v2 - [Bun](https://bun.sh/) 1.3 or later - Git and curl - At least 5 GiB of free disk space - These local ports free: 3000, 4010, 4110, 4111, 4112, 5432, 5433, and 6379 Make is optional. Each `make` command in this section is a shortcut for a `docker compose` or `bun` command defined in the repository's `Makefile`. ## Start the node 1. Clone the repository: ```bash git clone https://github.com/0xIntuition/intuition-core cd intuition-core ``` 2. Run the bootstrap script: ```bash scripts/bootstrap.sh ``` The script checks for Docker, Bun, and free disk space. It then creates `.env` from `example.env`, installs dependencies, builds and starts the services in the background, and waits up to two minutes for the API to respond. `make bootstrap` runs the same script. The first run builds every service image from source, so it takes longer than later runs. When the node is ready, the script prints: ```text OK API is healthy ``` ## Verify the node 1. Check that the API is healthy: ```bash curl http://localhost:3000/health ``` ```json { "status": "ok" } ``` 2. Check the contents of the knowledge graph: ```bash curl http://localhost:3000/api/stats ``` ```json { "data": { "atoms": 0, "triples": 0, "accounts": 0, "predicates": 14 } } ``` A new node has no atoms. The 14 predicates are the baseline set that the database migration adds. See [Predicates](https://learn.intuition.box/docs/concepts/predicates). To test the whole stack end to end, run the built-in smoke tests: ```bash make smoke # Mints an API key, creates atoms and a triple, waits for the workers make smoke-index # Indexes a 500-block window of the public testnet ``` Each smoke test starts its own temporary Compose project on random ports and removes it when it finishes, so it can run alongside your node. ## What is running | Service | Host port | Role | | ------------------------ | --------- | ---------------------------------------------------------------------------------------- | | `postgres-kg` | 5432 | The knowledge graph database: atoms, triples, accounts, predicates, and API keys | | `timescale` | 5433 | The event store database: chain events and the read models built from them | | `redis` | 6379 | Redis, reserved for coordinating indexer instances | | `migrate` | None | A one-time job that applies the knowledge graph schema and seeds the baseline predicates | | `timescale-migrate` | None | A one-time job that applies the event store schema | | `api` | 3000 | The query API. Reads are open and writes require an API key | | `atom-services` | 4010 | Classification and enrichment over HTTP, without a database | | `workers-parse` | 4110 | Parses new atoms | | `workers-enrichment` | 4111 | Enriches classified atoms with metadata | | `workers-classification` | 4112 | Classifies parsed atoms | The two migration jobs exit when they finish. That is expected. The workers have no API of their own; their ports serve health checks. Indexing adds two more services, described in [Index the chain](https://learn.intuition.box/docs/node/index-the-chain). ## Start without the bootstrap script ```bash cp example.env .env bun install --frozen-lockfile docker compose up -d ``` ## Stop the node ```bash make down ``` This stops every container and keeps your data. To start the node again, run `make up`. [Operate a node](https://learn.intuition.box/docs/node/operate-a-node) covers logs, health checks, upgrades, and backups. ## Next steps - [Write data](https://learn.intuition.box/docs/node/write-data) creates your first atoms and triples. - [Index the chain](https://learn.intuition.box/docs/node/index-the-chain) builds the graph from onchain events. - [Explorer](https://learn.intuition.box/docs/node/explorer) shows your node's health and data in a browser. - [Troubleshooting](https://learn.intuition.box/docs/node/troubleshooting) covers known failure modes. --- # Troubleshooting ## First checks 1. `curl http://localhost:3000/health` returns `{"status":"ok"}`. 2. `make status` shows every long-running service as `running`. 3. The logs of the failing service show no errors: `make logs SERVICE=`, where `` is a service such as `api` or `indexer`. [Operate a node](https://learn.intuition.box/docs/node/operate-a-node) lists every health endpoint. ## The bootstrap script reports that the API did not become healthy The script waits two minutes for the API. If the API is not healthy by then, inspect the services it depends on: ```bash docker compose logs api migrate postgres-kg ``` A failed `migrate` job stops the API from starting. Fix the error in its log, then run `docker compose up -d`. ## A port is already in use Another process holds the port. The bootstrap script warns about this before it starts the node. Stop the other process, or move the node's service to another host port in `.env`: | Port | Variable | | ---- | ------------------------- | | 3000 | `API_HOST_PORT` | | 4010 | `ATOM_SERVICES_HOST_PORT` | | 5432 | `POSTGRES_KG_HOST_PORT` | | 5433 | `TIMESCALE_HOST_PORT` | | 6379 | `REDIS_HOST_PORT` | Ports 4110 to 4112, 9091, 9092, and 8545 are fixed in Compose. If one of them is taken, stop the process that holds it. ## The API rejects a request | Status | Error | Cause and fix | | ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `401` | `api_key_required` | The default mode, `public-read`, requires a key for writes. [Mint one](https://learn.intuition.box/docs/node/write-data#mint-an-api-key) | | `401` | `invalid_api_key` | The key is mistyped or revoked. List keys with `bun run keys:list`, as described in [Manage keys](https://learn.intuition.box/docs/node/write-data#manage-keys) | | `403` | `api_key_not_writable` | The key is read-only. Create a key without `--read-only` | | `429` | `rate_limited` | The caller exceeded `API_RATE_LIMIT_RPM`. Wait for the time in the `Retry-After` header, raise the limit, or give the key its own `--rate-limit` | ## Postgres rejects the `TimeZone` parameter **Symptom.** The indexer and the projections fail immediately with `invalid value for parameter "TimeZone": "UTC"`. The API and the workers are unaffected. **Cause.** A corrupted Docker image layer. If the disk fills while Docker extracts the `timescaledb-ha` image, its time zone files can be written as zeros. Docker caches the damaged layer, so restarting does not help. **Fix.** Free disk space, then make Docker extract the image again: ```bash docker compose down docker rmi timescale/timescaledb-ha:pg17 docker pull timescale/timescaledb-ha:pg17 docker compose up -d ``` `docker compose down` keeps your data volumes. ## The indexer reports that `CHAIN_ID` must be set The indexer reads `CHAIN_ID` and the `MULTIVAULT_*` variables from its runtime environment. Set all four required chain variables in `.env`, then start the profile with `docker compose --profile indexing up -d`, which passes them to the container. If you run the indexer image yourself, set them on the container. See [Index the chain](https://learn.intuition.box/docs/node/index-the-chain#configure-the-chain). ## Nothing is indexed - Confirm that the block range contains `MultiVault` events. The block in which the contract was deployed is the right value for `MULTIVAULT_START_BLOCK`. - Check the indexer's progress for each event type: `curl -s http://localhost:9091/metrics | grep intuition_sync_progress_percent`. - If `MULTIVAULT_END_BLOCK` is set, the indexer stops at that block and exits with `Exited (0)`. This is expected. Only a nonzero exit code is a failure. ## Indexed atoms appear slowly The projections write atoms into the knowledge graph after the indexer stores the events. If they lag, check `curl http://localhost:9092/health/ready` and the projections' logs: ```bash docker compose --profile indexing logs --tail=100 projections ``` The projections resume from their checkpoints after a restart. ## Published images do not start - **Compose rejects `!reset`, or ignores the published images.** Update to a current Docker Compose v2. The legacy `docker-compose` v1 command does not support the published-image configuration. - **Compose reports that `INTUITION_CORE_IMAGE_TAG` is required.** Set a release version, as in `make up-published IMAGE_TAG=vX.Y.Z`, or set all six `INTUITION_CORE_*_IMAGE` variables. There is no `latest` fallback. See [Container images](https://learn.intuition.box/docs/node/container-images). ## Every container exits at once on macOS If every container shows `Exited (255)` at the same time, Docker Desktop stopped. Your data volumes are intact. Restart Docker Desktop, then run `docker compose up -d`. The migration jobs are safe to run again, and the projections resume from their checkpoints. ## Enrichment reports `skipped` This is correct for an atom that has nothing to enrich, such as a plain string. Enrichment produces artifacts for atoms that refer to a URL. A provider whose API key is not set is skipped rather than failed. See [Enrichment providers](https://learn.intuition.box/docs/extend/enrichment-providers). ## Related - [Configuration](https://learn.intuition.box/docs/node/configuration) - [Operate a node](https://learn.intuition.box/docs/node/operate-a-node) - [Index the chain](https://learn.intuition.box/docs/node/index-the-chain) --- # Write data The query API accepts atoms and triples and stores them in your node's knowledge graph. Each write is attributed to the account that owns the API key used to make it. > [!IMPORTANT] > Writing through the API stores data in your node only. It does not send a > transaction. To register a term with the protocol, see > [Publish onchain](https://learn.intuition.box/docs/build/publish-onchain). ## Before you begin - A running node. See [Run a node](https://learn.intuition.box/docs/node/run-a-node). - A wallet address to own your API key. Any address works for local testing. ## Authentication modes The `API_AUTH` setting controls which requests need a key. | `API_AUTH` | Reads | Writes | | ----------------------- | ------------- | --------------------- | | `public-read` (default) | Open | Require a key | | `gated` | Require a key | Require a key | | `open` | Open | Open and unattributed | To change the mode, set `API_AUTH` in `.env`, then run `docker compose up -d` to apply it. > [!WARNING] > Use `open` only for disposable experiments on your own machine. Never use it > on a shared, remote, or tunneled host. ## Mint an API key A key is bound to an account, identified by a wallet address. Replace the address below with your own. ```bash make keys ACCOUNT=0x0000000000000000000000000000000000000001 KEY_NAME=me ``` The command prints a key that begins with `ik_`. Copy it now: it is shown only once and cannot be recovered, because the node stores only its SHA-256 hash. Store the key in a shell variable for the commands on this page: ```bash API_KEY=ik_... ``` ### Manage keys To create, list, and revoke keys directly, run the key script from `services/api`. It needs the knowledge graph's connection string, which `make keys` otherwise supplies for you. ```bash cd services/api export DATABASE_KG_URL=postgresql://intuition:intuition@localhost:5432/intuition_kg bun run keys:create -- --name partner --account 0x0000000000000000000000000000000000000002 bun run keys:list bun run keys:revoke -- --id key_... ``` | Option for `keys:create` | Effect | | ------------------------ | ------------------------------------------------------------------------------------- | | `--read-only` | The key can read but not write. Useful when `API_AUTH` is `gated` | | `--rate-limit ` | Requests per minute for this key, overriding `API_RATE_LIMIT_RPM`. `0` means no limit | `keys:list` prints each key's identifier (`key_...`), which `keys:revoke` takes. ## Create an atom Send a URL, a string, or a JSON document of up to 64 KB as `input`: ```bash curl -X POST http://localhost:3000/api/atoms \ -H "Authorization: Bearer $API_KEY" \ -H 'Content-Type: application/json' \ -d '{"input":"https://github.com/oven-sh/bun"}' ``` ```json { "data": { "id": "0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3", "created": true, "createdBy": "0x0000000000000000000000000000000000000001" } } ``` The atom's [identifier](https://learn.intuition.box/docs/concepts/identifiers) is derived from its data, so it is the same identifier the protocol uses onchain. The request is idempotent: the first call returns `201`, and sending the same input again returns `200` with the same identifier. ## Watch the pipeline Within seconds, the workers parse, classify, and enrich the new atom. Fetch it to see each stage's status: ```bash curl http://localhost:3000/api/atoms/0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3 ``` ```json { "data": { "id": "0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3", "rawType": "http_uri", "classificationType": "SoftwareSourceCode", "parseStatus": "completed", "classificationStatus": "completed", "enrichmentStatus": "completed" } } ``` A stage can report `pending` briefly before it reports `completed`. The response contains more fields than shown here. ## Create a triple A [triple](https://learn.intuition.box/docs/concepts/triples) links a subject, a predicate, and an object by their identifiers. Each must be a 32-byte identifier: `0x` followed by 64 hexadecimal digits. 1. Create two more atoms, `is a` and `JavaScript runtime`, the same way you created the first. Their identifiers are: | Atom | Identifier | | -------------------- | -------------------------------------------------------------------- | | `is a` | `0xeab711d77f94dc65d29e0ffaadacb229b55773e317bd4401ef7b01662f75c0c6` | | `JavaScript runtime` | `0x4d838c07c7eba82f48f58fa11a611937850769a6b3954b3fc49a6d9cf60aad74` | 2. Create the triple: ```bash curl -X POST http://localhost:3000/api/triples \ -H "Authorization: Bearer $API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "subject_id": "0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3", "predicate_id": "0xeab711d77f94dc65d29e0ffaadacb229b55773e317bd4401ef7b01662f75c0c6", "object_id": "0x4d838c07c7eba82f48f58fa11a611937850769a6b3954b3fc49a6d9cf60aad74" }' ``` ```json { "data": { "id": "0xbc5250230227e7762a141c41c55aced479175a155fbd89f9778997c1e15d9df6", "created": true, "createdBy": "0x0000000000000000000000000000000000000001" } } ``` Like an atom, a triple's identifier is derived from its content, and the request is idempotent. > [!NOTE] > This example uses a plain string as the predicate to keep it short. > Applications use the canonical predicates, which have fixed identifiers. See > [Predicates](https://learn.intuition.box/docs/concepts/predicates). ## Read data back ```bash curl "http://localhost:3000/api/atoms?limit=5" curl http://localhost:3000/api/atoms/0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3/triples curl http://localhost:3000/api/atoms/0x951d18ba45fdf5ef5c3304ea3ae46a45e00b68464e2b1561cce4b48ffa1c09d3/artifacts ``` The first request lists recent atoms. The second returns every triple that uses the atom as its subject, predicate, or object. The third returns the artifacts that enrichment attached to the atom. ## Related - [REST API](https://learn.intuition.box/docs/build/rest-api) documents every endpoint and error. - [Atom services](https://learn.intuition.box/docs/build/atom-services) classifies and enriches input without storing anything. --- # Architecture Intuition Core turns `MultiVault` events and submitted input into a knowledge graph you can query. It has one pipeline, two databases, and one rule that holds throughout: a term's identifier depends only on its content. ## Components | Component | Implementation | Responsibility | | ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------- | | Indexer | Rust, built on rindexer | Decodes `MultiVault` events and stores them in the event store | | Projections | Rust | Build read models from stored events, and write onchain atoms and triples into the knowledge graph | | Workers | TypeScript on Bun | Parse, classify, and enrich each atom. Three workers, one per stage | | Query API | TypeScript, Hono | Serves the knowledge graph over REST and accepts attributed writes. See [REST API](https://learn.intuition.box/docs/build/rest-api) | | Atom services | TypeScript, Hono | Classifies and enriches input over HTTP without storing it. See [Atom services](https://learn.intuition.box/docs/build/atom-services) | | Explorer | TypeScript, React | A dashboard that reads the query API. See [Explorer](https://learn.intuition.box/docs/node/explorer) | ## Data flow ```diagram Chain events │ ▼ Indexer │ ▼ Event store │ ▼ Projections ─────────▶ Market read models │ ▼ Knowledge graph ◀───── Query API writes │ ▼ Workers: parse ▶ classify ▶ enrich │ ▼ Query API reads ``` Atoms enter the knowledge graph by two routes: from the chain, through the indexer and the `core_entities` projection, and from clients, through the query API. Both routes use the same identifiers, so a term that arrives by both routes is one record. Each new atom then passes through the three workers. They read the atom from the knowledge graph and write their results back to it, and the query API serves the result. ## The two databases **The knowledge graph** (`intuition_kg`) holds atoms, triples, accounts, predicates, enrichment artifacts, API keys, and an activity log. Triples are indexed by all six orderings of subject, predicate, and object, so a query can traverse a triple from any position. Graph reads are served from PostgreSQL. Core does not depend on a separate graph database. **The event store** (`intuition_timescale`) holds an append-only log of chain events, a typed table for each event type, and the read models that projections maintain: terms, vaults, positions, share prices, signals, leaderboards, and protocol totals. Both databases run on PostgreSQL with the TimescaleDB extension. They share no foreign keys and are linked by identifier. See the [Data model](https://learn.intuition.box/docs/reference/data-model). ## Deterministic identity An atom's identifier is `keccak256(ATOM_SALT, keccak256(data))`. A triple's is `keccak256(TRIPLE_SALT, subjectId, predicateId, objectId)`. Core derives identifiers with `@0xintuition/ids` and tests the results against known values. The `MultiVault` contract uses the same derivation, so a term's identifier in a node is its identifier onchain. See [Identifiers](https://learn.intuition.box/docs/concepts/identifiers). ## Reliability | Component | Mechanism | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Indexer | Indexes a fixed distance behind the head of the chain to limit exposure to reorganizations. Accepts a bounded block range for test runs | | Projections | A checkpoint per projection, so a restart resumes exactly where it stopped. Retries with backoff and circuit breakers. An event that fails permanently is recorded in `projection_dead_letter` and holds the checkpoint until you resolve it | | Workers | Each stage claims an atom under a lease. If a worker stops, the lease expires and another worker claims the atom. Attempts per stage are capped | | Migrations | Two one-shot jobs, one per database, run before the services start. Both are safe to run again | ## Authentication API keys are infrastructure credentials, not user accounts. Each key is bound to an account, and every write made with it is attributed to that account. The database stores only the SHA-256 hash of each key. The `API_AUTH` setting decides whether reads, writes, or both require a key. See [Write data](https://learn.intuition.box/docs/node/write-data). User authentication, billing, and product features are outside Core. Core is the open backend. You supply the application. ## Related - [How Intuition works](https://learn.intuition.box/docs/get-started/how-it-works) - [Run a node](https://learn.intuition.box/docs/node/run-a-node) - [Indexing scope](https://learn.intuition.box/docs/extend/indexing-scope) --- # Data model An Intuition Core node has two PostgreSQL databases. To connect to them, see [Query with SQL](https://learn.intuition.box/docs/build/query-with-sql). | Database | Name | Holds | | --------------- | --------------------- | -------------------------------------------------------------------------------------------------- | | Knowledge graph | `intuition_kg` | Atoms, triples, accounts, predicates, artifacts, API keys, and an activity log, in the `kg` schema | | Event store | `intuition_timescale` | Chain events and the read models built from them | The two are intentionally separate. You can use the knowledge graph without indexing the chain. When you do index the chain, projections replay its events and write the resulting atoms and triples into the knowledge graph. ## Knowledge graph tables ### `kg.nodes` Atoms. The table is named `nodes` for historical reasons; each row is one atom. | Column | Meaning | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | | `id` | The atom's identifier | | `raw_type` | The form of the atom data: `string`, `json`, `http_uri`, `ipfs_uri`, or `iid` | | `data`, `data_hex` | The atom data as text and as hexadecimal bytes | | `data_resolved` | Content resolved during processing | | `status` | `active` or `draft` | | `visibility` | `public` or `unlisted`. The API serves only records that are `active` and `public` | | `created_by` | The account that created the atom, when known | | `is_onchain` | Whether the atom has been observed onchain | | `parse_status`, `classification_status`, `enrichment_status` | The state of each processing stage: `pending`, `processing`, `completed`, `failed`, or `skipped` | | `parse_result`, `classification_result` | The output of the parse and classify stages | | `classification_type` | The atom's normalized type, such as `Person`, used for filtering. `Unknown` until classified | | `iid` | The atom's Intuition Identifier, set only when IID processing is enabled | | `search_text` | Text the API's search matches against | Each stage also has columns for its attempt count, error, lease expiry, and timestamps, such as `parse_attempts`, `parse_error`, and `parse_lease_expires_at`. ### `kg.triples` Claims between terms. | Column | Meaning | | -------------------------------------------------- | ------------------------------------------------------------------- | | `id` | The triple's identifier | | `subject_id`, `predicate_id`, `object_id` | The identifiers of the three terms | | `subject_type`, `predicate_type`, `object_type` | `node` if the position holds an atom, `triple` if it holds a triple | | `is_counter_triple`, `sibling_triple_id` | The link between a triple and its counter-triple | | `status`, `visibility`, `created_by`, `is_onchain` | As for atoms | | `edge_kind` | The kind of edge. `claim` by default | | `source`, `source_uri`, `confidence`, `inferred` | Provenance. `confidence` is between 0 and 1 | | `provenance`, `metadata` | JSON fields for further provenance and metadata | The table has an index for each of the six orderings of subject, predicate, and object, so you can find every triple that mentions a term in any position. ### Other tables | Table | Holds | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kg.accounts` | Accounts, keyed by wallet address | | `kg.predicates` | The predicate registry: `slug`, `label`, `description`, `inverse_predicate_id`, and the flags `is_transitive`, `is_symmetric`, `is_hierarchical`, `is_social`, and `is_market`. A new node is seeded with 14 predicates | | `kg.artifacts` | Enrichment output, one row per artifact: `node_id`, `artifact_kind` (the artifact type), `artifact_version`, `status`, `source_uri`, `data`, `extracted`, and `error` | | `kg.node_contexts` | Context URIs registered onchain for an atom through `AtomContextRegistered` events. `uri_hex` holds the exact bytes. `uri_text` is a readable copy when the bytes are valid UTF-8 | | `kg.node_identifiers` | Additional Intuition Identifiers for an atom, written only when IID processing is enabled | | `kg.events` | An append-only log of activity in the graph: `event_time`, `actor_id`, `entity_kind`, `entity_id`, `event_type`, and a JSON `payload`. `kg.events_hourly` rolls it up by hour | | `kg.api_keys` | API keys: `id` (such as `key_...`), `key_hash` (the SHA-256 hash of the key), `name`, `account_id`, `can_write`, `rate_limit_rpm`, `revoked_at`, and `last_used_at` | The schema also defines `kg.adjacency`, `kg.node_urls`, `kg.node_stats`, `kg.account_stats`, `kg.predicate_stats`, and `kg.triple_pattern_stats`. No Core service writes to these tables yet, so they are empty on a standard node. For traversal, query `kg.triples`. ## Event store tables ### `event_store` The canonical log of chain events. | Column | Meaning | | ----------------------------------------------- | --------------------------------------------------------------------------------------- | | `sequence_number` | The order in which the event was ingested | | `block_number`, `block_timestamp`, `block_hash` | The block | | `transaction_hash`, `log_index` | The event's position in the chain | | `event_type`, `event_data` | The event's name, such as `Deposited`, and its decoded fields | | `term_id` | The term the event concerns, when it has one | | `entity_id` | The account the event concerns: the receiver of a deposit or the sender of a redemption | | `is_canonical` | Whether the event is on the canonical chain | > [!NOTE] > `sequence_number` follows ingestion order, which is not strictly chain > order across event types. To reconstruct chain order, sort by > `block_number` and `log_index`. ### Typed event tables One table for each event type the indexer ingests: `atom_created_events`, `triple_created_events`, `deposited_events`, `redeemed_events`, `share_price_changed_events`, `protocol_fee_accrued_events`, and `atom_context_registered_events`. In these tables, `term_id` is a number. The `term_id_hex` column holds the same identifier in the hexadecimal form that the read models and the knowledge graph use. ### Read models | Table | Holds | | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `term` | Terms registered onchain. `term_type` is `atom` or `triple` | | `term_summary`, `term_market_cap_history`, `predicate_object_summary`, `subject_predicate_summary` | Market totals for each term, and rollups by relationship | | `vault` | One row per term and curve ID: total shares, total assets, current share price, market cap, and holder count | | `share_price_history` | Share prices over time | | `position`, `position_change` | Each account's shares by term and curve ID, and every change to them | | `active_vault_position` | The accounts that currently hold shares in each vault | | `signal` | Each deposit or redemption as a change in signal, by account, term, and curve ID | | `event`, `deposit_fact`, `redemption_fact`, `fee_transfer_fact` | Normalized facts for analytics | | `account`, `account_pnl_state`, `account_pnl_snapshot` | Accounts, and their profit and loss over time | | `leaderboard_cache`, `leaderboard_cache_version` | Versioned leaderboards | | `stats`, `stats_history` | Protocol totals and their history | | `projection_checkpoints` | The last event each projection processed | | `projection_dead_letter` | Events a projection could not process, held for an operator to inspect | The event store also contains tables for product analytics. A standard node does not fill them. ## How the databases relate The databases are linked by identifier, not by foreign key. - An atom's `kg.nodes.id` equals its `term_id` in the read models. - A triple's `kg.triples.id` equals its `term_id` in the read models. To combine data from both, query each database and join on the identifier in your application. ## Notes - The query API's `GET /api/schema` returns the knowledge graph's tables, columns, keys, and indexes as JSON. - Some event store objects, including continuous aggregates, views, and compression policies, are defined only in SQL migrations, in `migrations/timescale/`. Read the migration before you depend on one. ## Related - [Query with SQL](https://learn.intuition.box/docs/build/query-with-sql) - [Architecture](https://learn.intuition.box/docs/reference/architecture) - [Glossary](https://learn.intuition.box/docs/reference/glossary) --- # Glossary | Term | Definition | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Account | An address that creates terms or holds shares. Intuition Core keys accounts by wallet address, and attributes each API write to the account of the key that made it. | | Agent skill | A package of reference material that a coding agent loads when a task needs it. See [Agent skills](https://learn.intuition.box/docs/agents/agent-skills). | | API key | A credential for a node's query API. It begins with `ik_` and is bound to an account, to which every write made with it is attributed. The node stores only its SHA-256 hash. | | Artifact | A typed record that an enrichment plugin attaches to an atom, such as OpenGraph data or a Wikipedia extract. Core stores artifacts in `kg.artifacts`. | | Artifact type | The name of a kind of artifact, such as `opengraph` or `npm-package`. Each artifact type has a registered data schema. | | Atom | The basic unit of data. An atom identifies one thing. Its identifier is derived from its data. See [Atoms](https://learn.intuition.box/docs/concepts/atoms). | | Atom data | The bytes that define an atom: a URL, an IPFS URI, a JSON document, a plain string, or raw bytes. | | Atom services | A stateless HTTP service that classifies and enriches input without storing it. See [Atom services](https://learn.intuition.box/docs/build/atom-services). | | Atom wallet | A smart contract wallet that the protocol associates with each atom. The `AtomCreated` event records its address. | | Bonding curve | The function that sets the price of a vault's shares. On the default curve, the share price rises only as fees accumulate in the vault. On a progressive curve, it rises as more shares are issued. See [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals). | | Checkpoint | The record of the last event a projection processed. After a restart, the projection resumes from its checkpoint. | | Classification | The stage that determines what an atom refers to, expressed as a domain and a subtype, such as `github` and `repo`. Plugins perform it. | | Counter-triple | The term that represents disagreement with a triple. It has its own identifier and vault. | | Curve ID | The number that identifies a bonding curve in the `BondingCurveRegistry` contract. A term has a separate vault on each curve. On mainnet, curve ID 1, the Linear Curve, is the default. | | Deposit | A transfer of TRUST into a term's vault in exchange for shares. A deposit is a signal. | | Enrichment | The stage that gathers metadata about an atom from external sources and stores it as artifacts. | | ERC-8004 | An Ethereum standard that defines onchain registries for the identity, reputation, and validation of autonomous agents. See [ERC-8004 agent layer](https://learn.intuition.box/docs/agents/erc-8004). | | Event store | The database that holds the append-only log of chain events and the read models built from it. | | Explorer | The dashboard included with Intuition Core. It reads a node's query API. See [Explorer](https://learn.intuition.box/docs/node/explorer). | | Identifier | A 32-byte value that names a term, derived from the term's content. See [Identifiers](https://learn.intuition.box/docs/concepts/identifiers). | | Identity strength | How an enrichment plugin found a resource: by an external `identifier`, by a direct `url`, or by a `title` search. A title search is the weakest of the three. | | Indexer | The component of Intuition Core that decodes `MultiVault` events into the event store. | | Indexing scope | The set of choices that decide what a node ingests, projects, processes, and serves. See [Indexing scope](https://learn.intuition.box/docs/extend/indexing-scope). | | Intuition Core | The open-source backend: an indexer, projections, workers, a query API, atom services, and an explorer. | | Knowledge graph | The database that holds atoms, triples, accounts, predicates, and artifacts. | | Lease | A time-limited claim that a worker holds on an atom while it runs one stage. If the worker stops, the lease expires and another worker claims the atom. | | MultiVault | The protocol contract that registers terms and holds their vaults. | | Node | An instance of Intuition Core. In the knowledge graph's schema, `nodes` is also the name of the table that holds atoms. | | Parse | The first processing stage. It determines what form an atom's data takes, such as a URL, a JSON document, or an ENS name. | | Plugin | A module that adds classification or enrichment coverage to a node. The built-in plugins use the same interface as yours. See [Classification plugins](https://learn.intuition.box/docs/extend/classification-plugins). | | Position | The shares an account holds in one term's vault on one curve. | | Predicate | An atom that names the relationship a triple expresses. See [Predicates](https://learn.intuition.box/docs/concepts/predicates). | | Preset | A named set of choices. An enrichment preset selects plugins, such as `default` or `music`. An indexing scope preset selects events, projections, and processing, such as `kg-only`. | | Projection | A process that builds a read model from the event store and records its progress in a checkpoint. | | Query API | Intuition Core's REST API, which serves the knowledge graph and accepts writes. See [REST API](https://learn.intuition.box/docs/build/rest-api). | | Read model | A table that a projection derives from chain events, such as `vault` or `position`. | | Redemption | The return of shares to a vault in exchange for TRUST. | | Share | A unit of ownership in a vault. A deposit issues shares and a redemption returns them. The bonding curve sets their price. | | Signal | A deposit into a term's vault, read as a public statement that the depositor stands behind the term. A deposit on a counter-triple signals disagreement with the triple. | | Term | An atom or a triple. Every term has an identifier and, once registered, a vault. | | Triple | A claim that links a subject, a predicate, and an object. See [Triples](https://learn.intuition.box/docs/concepts/triples). | | Trust provider | An organization that publishes assessments of ERC-8004 agents into the graph. | | TRUST | The native currency of the Intuition network. On the testnet it is tTRUST. | | Vault | The pool, held by `MultiVault`, into which TRUST is deposited for a term on one curve. See [Vaults and signals](https://learn.intuition.box/docs/concepts/vaults-and-signals). | | Workers | The Intuition Core services that parse, classify, and enrich atoms, one stage each. |