Kepto brings real assets on-chain: tokenized stocks backed one to one, real-world assets bound to provable documents, and verifiable storage, all settled in KEP on Robinhood Chain. This is how every piece works, from the backing invariant down to the byte-level Merkle rules and the HTTP API.
Kepto puts real assets on-chain with cryptographic proof of what backs them. It is three products on one settlement layer: tokenized stocks backed one to one, real-world assets bound to provably held documents, and the verifiable storage that makes those proofs real. Everything is priced and settled in KEP on Robinhood Chain, an EVM-compatible Arbitrum Orbit L2.
The difference from a normal broker or storage host is that the backing is readable on-chain at any moment, not promised in a quarterly PDF. Supply can never exceed attested reserves, documents are audited by random challenges, and retrieved bytes are re-hashed against an on-chain root before they reach you.
| Pillar | What it is | Backed by |
|---|---|---|
| Tokenized stocks | ERC-20 shares of real companies (dAAPL, dTSLA) you can trade 24/7. | Attested reserves, capped in-contract by Secure Mint. |
| Real-world assets | Asset NFTs for property, invoices and bonds, fractionalizable into shares. | Documents held under provable on-chain custody. |
| Verifiable storage | Merkle-committed storage deals with challenges and slashing. | Provider collateral and economic proofs of possession. |
| Actor | Role |
|---|---|
| Holder / investor | Mints, trades, stakes and redeems tokenized shares, or stores and verifies files. Settles in KEP. |
| Issuer / operator | Attests reserves and NAV, fills mint and redeem orders against off-chain custody, distributes dividends. |
| Provider | Stores data. Stakes 1,000 KEP with a public endpoint, locks per-deal collateral, answers storage challenges. |
| Challenger | Anyone. Posts a 10 KEP bond to force a proof of possession, and is paid from slashed collateral if it fails. |
| Contract | Purpose |
|---|---|
KeptoToken | ERC-20 KEP settlement token, 18 decimals, testnet faucet. |
EquityToken | Tokenized stock: reserves, Secure Mint, mint/redeem queue, NAV, KEP dividends. |
ComplianceRegistry | Allowlist / blacklist, freeze and exempt, gates share transfers. |
EquityVault | Stakes shares into position NFTs and streams KEP emission APR plus dividends. |
RwaRegistry / RwaVault | Assets backed by storage deals, attestation, and fractionalization. |
StorageMarket / ProviderRegistry | Deals, escrow vesting, challenges, proofs, slashing. |
| Surface | Best for |
|---|---|
| dApp | Humans with a browser wallet: trade, stake, store, verify, interactively. |
| HTTP API | Services and scripts: a hosted gateway turns plain HTTP into on-chain deals. |
| SDK & CLI | TypeScript apps and terminals: full programmatic control of every flow. |
A tokenized stock is an ordinary ERC-20 that represents a share of a real company, one token per share. Kepto ships two demo tickers, dAAPL and dTSLA, each an EquityToken priced and traded in KEP. What makes it trustworthy is not a promise, it is an invariant enforced by the contract.
Each token carries an on-chain reserves figure the issuer attests to. Every mint runs through Secure Mint, which reverts unless totalSupply() + amount <= reserves. A share that is not covered by reserves simply cannot be issued, and reservesRatioBps() reports the live backing ratio for anyone to read.
Transfers pass through a shared ComplianceRegistry. In blacklist mode (the demo default) tokens move freely except for frozen accounts, so shares are composable across DeFi. Flip to allowlist mode and only verified addresses can hold, the permissioned pattern regulated issuers need. The registry also supports freeze, exempt, and delegated verification agents.
Primary issuance uses an on-chain order queue so KEP and shares are only ever escrowed by the contract, never by a person.
requestMint(paymentKep); the contract escrows your KEP and records a pending order. Redeeming is the mirror, requestRedeem(shares) escrows your shares.fillMint issues shares through Secure Mint and forwards your KEP to the treasury; fillRedeem burns the shares and pays you KEP. An operator keeper fills continuously so this settles in seconds.cancelOrder(id) and get your escrow back to the wei.navPerShare records the attested net asset value per share. Dividends are paid in KEP through a pull-model distributor: distributeDividend(amount) splits pro rata across every holder, withdrawableDividend(holder) reads what you are owed, and claimDividend() pays it out. The accounting corrects on every transfer, so income follows the shares even as they move.
| Enforced on-chain | Trusted off-chain |
|---|---|
| Supply can never exceed attested reserves | Real custody and legal title of the shares |
| Transfer restrictions and freezes | Truthfulness of the reserves attestation |
| Mint and redeem authorization and escrow | Broker-dealer and transfer-agent licensing |
| Dividend split and claim math | KYC verification of holders |
# read a tokenized stock: reserves, backing, NAV, supply
KEPTO_CHAIN=robinhoodTestnet kepto equity token <dAAPL>
# request a mint by escrowing KEP (an operator fills it)
KEPTO_PRIVATE_KEY=0x... kepto equity mint <dAAPL> 300
# claim your KEP dividends
KEPTO_PRIVATE_KEY=0x... kepto equity claim <dAAPL>Stake tokenized shares in the EquityVault to earn yield without giving up ownership. Staking locks your shares and mints an ERC-721 position NFT that represents the stake. Your yield comes from two streams at once.
| Stream | Where it comes from |
|---|---|
| KEP emissions | A funded reward pool per stock streams KEP to stakers per second, Synthetix-style, over a set duration. |
| Dividend passthrough | Dividends the staked shares accrue are harvested by the vault and forwarded to stakers pro rata, so staking never costs you dividend rights. |
APR is read straight from chain state, never a marketing number. The vault exposes aprBps(stock), which annualizes the current emission rate against the value of everything staked, priced at navPerShare. As more shares stake, the same emission spreads wider and the APR falls; the figure the dashboard shows is exactly what the contract returns.
Pending rewards are attached to the position NFT itself. Transfer the NFT and its accrued yield goes with it, so positions are tradable instruments, not just receipts. earned(positionId) reports the live reward and dividend, claimRewards(positionId) pays them out, and unstake(positionId) auto-claims everything and returns your exact shares.
periodFinish until topped up, so APR varies with total staked and with how much reward is funded. Rewards that accrue while nothing is staked are not distributed.# see the live APR, total staked and reward rate for a stock
KEPTO_CHAIN=robinhoodTestnet kepto equity pool <dAAPL>
# stake shares, then check and claim what you have earned
KEPTO_PRIVATE_KEY=0x... kepto equity stake <dAAPL> 10
KEPTO_PRIVATE_KEY=0x... kepto equity rewards <positionId>
KEPTO_PRIVATE_KEY=0x... kepto equity claim-rewards <positionId>A deal is a fixed-term contract between one client and one provider over one file commitment. Its lifecycle:
Created ──acceptDeal──▶ Accepted ──activateDeal──▶ Active ──▶ Completed
│ │ │
└──cancelDeal────────────┴──cancel (window lapsed) └──▶ Terminated
▼ (3 faults)
CancelledThe client calls createDeal(provider, merkleRoot, leafCount, sizeBytes, duration, totalPrice) and escrows totalPrice KEP into the market. The provider must be active in the registry, the commitment must be internally consistent (leafCount == ceil(sizeBytes / 4096)) and the duration must be within 1-360 days.
The provider has a 24-hour acceptWindow to call acceptDeal, locking collateral equal to 50% of the deal price (collateralBps = 5000). The slash amount per fault is set to collateral / maxFaults.
The client uploads the raw file bytes to the provider's HTTPS endpoint. The provider re-chunks the body, recomputes the Merkle root, checks it against the on-chain deal and then calls activateDeal within the 24-hour activationWindow. This starts the clock: endTime = now + duration.
vested = totalPrice · (min(now, endTime) − startTime) / duration. The provider can pull the vested balance at any time with withdrawEarnings.endTime, anyone can call settleDeal (as long as no challenge is open): the provider receives the remaining escrow plus its collateral back and the deal completes.cancelDeal while the deal is Created, or once an Accepted deal's activation window has lapsed. Escrow is refunded; collateral (if locked) returns to the provider.| Parameter | Default | Meaning |
|---|---|---|
acceptWindow | 24 h | Provider must accept within this window after creation. |
activationWindow | 24 h | Provider must activate within this window after accepting. |
collateralBps | 5,000 (50%) | Collateral as a share of the deal price. |
minDealDuration | 1 day | Shortest allowed term. |
maxDealDuration | 360 days | Longest allowed term. |
challengeBond | 10 KEP | Bond posted to open a challenge. |
challengeCooldown | 12 h | Minimum spacing between challenges per deal. |
proofWindow | 8 h | Provider's deadline to answer a challenge. |
maxFaults | 3 | Faults that terminate the deal. |
Storage without proofs is a promise. Kepto makes the promise expensive to break with a challenge game anyone can play.
While a deal is Active (and before endTime), anyone except the provider may call challenge(dealId), posting the 10 KEP bond. The call records only the commitBlock, it does not pick the challenged chunk. The chunk is fixed later from that block's hash, which nobody can know when the challenge is submitted:
index = keccak256(blockhash(commitBlock) ‖ dealId ‖ faultCount) mod leafCountBecause the index is unknowable at challenge time and the provider is barred from challenging itself, a provider cannot steer the challenge onto chunks it happens to hold. Challenges are rate-limited to one per deal per 12 hours (challengeCooldown), and only one may be open at a time.
The provider, or anyone relaying on its behalf, answers with submitProof(dealId, chunk, proof): the raw challenged chunk and its Merkle sibling path. The market verifies it against the deal's committed root at the challenged index. On success the challenge clears and the bond is paid to the provider as gas compensation.
If the 8-hour proofWindow lapses unanswered, anyone can call reportFault(dealId):
slashPerFault = collateral / maxFaults is burned from the provider's collateral (capped by what remains).The data commitment is normative and shared by every component, the Solidity verifier (StorageProofs.sol), the TypeScript reference (@kepto/core), the provider daemon and this web app. They must agree byte-for-byte.
| Rule | Definition |
|---|---|
CHUNK_SIZE | 4096 bytes. The final chunk may be shorter (1..4096). Zero-length files are not storable. |
leafCount | ceil(size / 4096), and it must hold that (size + 4095) / 4096 == leafCount. |
Leaf hash | keccak256(0x00 ‖ chunk_bytes), a single 0x00 domain tag prepended to the raw chunk bytes. |
Node hash | keccak256(0x01 ‖ left ‖ right), a single 0x01 tag, then the two 32-byte child hashes. |
Odd levels | When a level has an odd number of nodes, the last node is promoted unchanged to the next level, no duplication. |
Single chunk | The merkleRoot of a single-chunk file is its leaf hash. |
A proof is an array of sibling hashes from the leaf level upward. At any level where the current node is the promoted lone last node, no sibling is consumed. Verification walks:
h = leafHash(chunk); idx = index; size = leafCount; pi = 0
while size > 1:
if idx == size-1 and size is odd:
pass
else:
sib = proof[pi]; pi += 1
h = idx % 2 == 0 ? node(h, sib) : node(sib, h)
idx = idx / 2
size = (size + 1) / 2
require pi == len(proof) and h == rootEvery proof element must be consumed, over-long proofs are invalid, which rules out proof-padding games.
| Language | Location |
|---|---|
| Solidity (on-chain verifier) | contracts/src/StorageProofs.sol |
| TypeScript (browser + node) | packages/core/src/merkle.ts |
In TypeScript: commit(data) returns { root, leafCount, sizeBytes, tree }, and proveLeaf(tree, index) / verifyLeaf(root, leafCount, index, chunk, proof) mirror the contract exactly. This app runs commit() in your browser both when storing and when verifying retrievals.
The RWA layer (RwaRegistry, RwaVault) tokenizes real-world assets as ERC-721s (Kepto RWA, IRWA) whose legal paperwork must be provably stored in active Kepto storage deals. Every document is bound to the asset by the Merkle root of a deal you own; if the documents stop being stored, anyone can flag the asset as lapsed on-chain.
registerAsset(name, assetType, metadataURI, declaredValue, dealIds[], labels[]) pays the registration fee and mints the NFT to the caller. Every dealId must be an Active StorageMarket deal owned by the caller; each becomes a document { dealId, merkleRoot, label } bound to the asset. More documents can be added later with addDocument while the asset is Registered.attest(assetId) after verifying the asset against the stored documents. Requires the backing to be intact. Each call records one per-attestor approval (AttestationRecorded); the asset flips to Attested once attestationCount reaches attestationsRequired. revokeAttestation(assetId, reason) withdraws the caller's own approval, while the quorum is still forming or after Attested, and drops an Attested asset back to Registered.declaredValue with updateValuation(assetId, newValue), emitting ValuationUpdated with the old and new values.flagLapsed(assetId). If any document's deal is no longer Active the asset becomes Lapsed and loses its attestation. Lapse-flagging keeps working while the asset is fractionalized, so share markets can see backing failures.renewDocument(assetId, docIndex, newDealId). The new deal must be Active, owned by the caller, and commit the same merkleRoot. A Lapsed asset with fully renewed backing returns to Registered (re-attestation required).fractionalize(assetId, totalShares, name, symbol) requires a current attestation (isAttestationCurrent, not merely status Attested): the NFT is locked in the RwaVault and an RwaShares ERC-20 minting totalShares to the caller is deployed. redeem(assetId) by a caller holding all shares burns them and returns the NFT.The registry owner sets the policy with setAttestationPolicy(validity, required): attestationsRequired (default 1) approvals from distinct attestors are needed before an asset becomes Attested, and an attestation stays fresh for attestationValidity (default 90 days, 0 means it never expires).
attestationCount(assetId), hasAttested(assetId, attestor) and isAttestationCurrent(assetId) expose the quorum and freshness state on-chain.flagStale(assetId): the asset returns to Registered with all approvals reset (AttestationExpired), and a fresh quorum is required.flagLapsed, on addDocument, and on a renewDocument that restores a Lapsed asset.RwaShares is a dividend token. Anyone, an issuer forwarding rent, a servicer paying coupons, can call distribute(amount) to pull KEP into the token (approve it first); the amount is split pro rata across holders at that moment via a per-share accumulator (accPerShare).
withdrawableOf(holder) and claimedOf(holder) report a holder's pending and already-claimed income; totalDistributed() is the lifetime total. claim() pays the pending amount out in KEP.redeem: selling or redeeming shares never forfeits income already earned, and a buyer does not inherit the seller's past distributions.| Value | Status | Meaning |
|---|---|---|
| 0 | None | No asset at this id. |
| 1 | Registered | Minted with documents bound to Active deals; awaiting attestation. |
| 2 | Attested | The attestor quorum verified the asset against the stored documents. Unlocks fractionalization while the attestation is current. |
| 3 | Lapsed | Some backing deal is no longer Active. Attestation is lost; renewing every broken document returns it to Registered. |
| 4 | Banned | Admin-terminal state for fraudulent assets. No way back. |
registerAsset transfers registrationFee KEP (default 100 KEP, owner-settable) into the registry. Collected fees are withdrawable by the registry owner. Approve the fee on the KEP token before registering, the Tokenize page does both steps for you.
Storage deals are finite; assets are not. Before a backing deal's endTime, the asset owner re-stores the exact same bytes, which produces the same merkleRoot, under a new deal, then calls renewDocument. Continuous coverage keeps the asset attestable; a gap lets anyone flag it Lapsed.
The attestor model works for deeds and invoices, things whose backing is a document. It cannot enforce the backing of a share: a paper claim that you hold 100 shares does nothing to stop you selling those shares the next minute, leaving the claim naked. Custody has to be enforced, not promised.
EquityVault answers this by staking on-chain stock tokens (Robinhood Chain's native tokenized equities). You lock the equity tokens in the vault and receive a transferable ERC-721 position (Kepto Equity Position, IEQP). Because the tokens sit in the vault, the backing is cryptographically enforced: you provably cannot sell the underlying while the position exists. What trades is the receipt, the position NFT, and unstake redeems it 1:1 for the exact staked amount, no fees and no oracle.
Only owner-listed tokens are stakeable (listStock / delistStock); delisting blocks new positions while existing ones unwind normally. On testnet the listed tokens are clearly-labeled DemoStock ERC-20s with a public faucet minting 100 tokens per address per 24h, so anyone can try the flow; a mainnet vault would whitelist the chain's native tokenized equities.
stake(stockToken, amount, metadataURI) pulls the tokens into the vault, mints the position NFT recording token, amount, opener and open time, and tracks totalLocked per stock.attachDocument(positionId, dealId, label) lets the position owner bind prospectus or terms documents held as live Kepto deals (same live-deal and caller-owns-deal rules as the RWA registry).unstake(positionId) by the position owner burns the NFT and returns the exact staked amount. The vault never creates equity exposure, it locks and receipts it.Views expose the state on-chain: getPosition, getDocuments, getStock, stockCount / stockAt, positionCount and stockSymbol. The staking flow runs through the Equity CLI below, while the Equity page in the app surfaces the tokenized equity model: per token backing, compliance status, mint and redeem orders, and stablecoin dividends.
| Command | What it does |
|---|---|
kepto equity stocks | List the vault's listed stocks with symbol, address and total locked. |
kepto equity stake <symbol|token> <amount> [--uri …] | Approve the stock token and stake it, minting a position NFT. |
kepto equity positions | List open positions with stock, amount, opener and owner. |
kepto equity show <positionId> | Show one position, its amount and attached documents. |
kepto equity unstake <positionId> | Burn the position and receive the exact staked amount back. |
kepto equity attach <positionId> <dealId> [label] | Bind one of your Active storage deals to the position as a document. |
kepto equity faucet <symbol|token> | Claim 100 demo stock tokens from a testnet stock's faucet. |
The gateway exposes the vault read-only under /api/v1/equity:
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/equity/stocks | Listed equity tokens with symbol, address and total locked. |
GET | /api/v1/equity/positions | Open positions with stock, amount, opener and owner. |
GET | /api/v1/equity/positions/:id | One position with its attached documents. |
The Kepto Gateway (packages/gateway) is a hosted integration API in the spirit of web3.storage: it holds a funded wallet and turns plain HTTP into storage deals. POST raw bytes and the gateway computes the Merkle commitment, picks the cheapest active provider with capacity (or the one you pin), escrows KEP, uploads the data and waits for on-chain activation, then hands you back the deal as JSON. Retrievals are re-verified against the on-chain root before a single byte is returned.
All endpoints live under /api/v1 (default port 8748). When the gateway is started with the API_KEYS environment variable set (comma-separated), every request except GET /api/v1/status and GET /api/v1/openapi.yaml must carry a bearer token:
Authorization: Bearer <your-api-key>With no API_KEYS configured the gateway is open. Errors are JSON: { "error": string }.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/status | Service metadata: chain, contract addresses, gateway wallet and its KEP balance. No auth. |
GET | /api/v1/providers | Registered storage providers with endpoint, capacity, usage and pricing. |
POST | /api/v1/files | Store a file. Raw application/octet-stream body. Query: duration (e.g. 12h, 30d; default 30d), price (total KEP; default derived from size × duration), provider (pin an address), replicas (default 1, store with n distinct providers). Optional x-filename header is echoed back. Returns 201 with the Deal JSON, 503 if no provider has capacity, or 413 if the upload would exceed your key's free-tier quota. |
GET | /api/v1/files/:dealId | Deal status for a stored file, including vested KEP and any open challenge. |
GET | /api/v1/files/:dealId/content | Retrieve the file. The gateway fetches from the provider, re-verifies against the on-chain Merkle root, and streams the bytes with an x-merkle-root header. Fails rather than returning unverified data. |
GET | /api/v1/deals | All deals created by the gateway wallet. |
POST | /api/v1/deals/:dealId/challenge | Post the bond and challenge the provider to prove possession. |
POST | /api/v1/deals/:dealId/settle | Settle an ended deal, pays the provider, returns collateral. |
POST | /api/v1/deals/:dealId/fault | Report an expired, unanswered challenge (slashes the provider). |
GET | /api/v1/openapi.yaml | The full OpenAPI 3.0 contract. No auth. |
curl -X POST "$GATEWAY/api/v1/files?duration=30d&price=5" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/octet-stream" \
-H "x-filename: backup.tar" \
--data-binary @backup.tarThe call blocks through the whole pipeline, commit, escrow, provider acceptance, upload, activation, and returns the live deal:
{
"dealId": 42,
"provider": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"merkleRoot": "0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658",
"sizeBytes": 1048576,
"leafCount": 256,
"status": "Active",
"durationSeconds": 2592000,
"totalPriceKep": "5",
"collateralKep": "2.5",
"filename": "backup.tar"
}curl -H "Authorization: Bearer $API_KEY" "$GATEWAY/api/v1/files/42"POST /api/v1/files?replicas=n stores the same commitment with n distinct active providers, cheapest first, creating one deal per provider and returning all deal ids. Retrieval tries the providers in order until one returns bytes matching the on-chain root, so a single surviving replica is enough to recover the file.
When API_KEYS is set, per-key upload quotas can be enforced with FREE_TIER_MAX_BYTES (0 = unlimited, the default). Usage is tracked per key in the gateway's quota store, and once a key's cumulative uploaded bytes would exceed the cap the gateway returns 413 quota exceeded.
curl -H "Authorization: Bearer $API_KEY" \
-o backup.tar "$GATEWAY/api/v1/files/42/content"The gateway is a single Node process (npm run start -w @kepto/gateway) configured entirely via environment variables:
| Variable | Meaning |
|---|---|
GATEWAY_PRIVATE_KEY | Required. The gateway wallet key, holds ETH for gas and KEP for escrows. |
KEPTO_CHAIN | local | robinhoodTestnet | robinhoodMainnet (default local). |
API_KEYS | Comma-separated bearer tokens. Empty = open access. |
PORT | Listen port (default 8748). |
DEFAULT_DURATION_SECONDS | Deal duration when ?duration is omitted (default 30 days). |
DEFAULT_PRICE_LUM_PER_GIB_30D | Price basis in KEP per GiB per 30 days used when ?price is omitted (default 10). |
FREE_TIER_MAX_BYTES | Cumulative upload cap per API key in bytes; 0 = unlimited (default). Requires API_KEYS. |
API_KEYS for anything reachable from outside localhost.@kepto/sdk is a viem-based TypeScript client for the whole protocol, usable from Node and the browser. It re-exports the chunking & Merkle primitives from @kepto/core and ships the kepto CLI.
import { readFile, writeFile } from "node:fs/promises";
import { parseEther } from "viem";
import { KeptoClient, chains } from "@kepto/sdk";
const client = new KeptoClient({
chain: chains.robinhoodTestnet,
privateKey: process.env.KEPTO_PRIVATE_KEY as `0x${string}`,
addresses: {
token: "0x…",
registry: "0x…",
market: "0x…",
},
});
const data = new Uint8Array(await readFile("backup.tar"));
const stored = await client.storeFile({
data,
provider: "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
durationSeconds: 30n * 86400n,
totalPrice: parseEther("5"),
onProgress: (m) => console.log(m),
});
console.log("deal", stored.dealId, "root", stored.root);
const deal = await client.getDeal(stored.dealId);
console.log("status", deal.status, "ends", deal.endTime);
const bytes = await client.retrieve(stored.dealId);
await writeFile("restored.tar", bytes);addresses may be omitted on chains with a generated deployment in @kepto/core; pass rpcUrl to override the chain default, or walletClient instead of privateKey to sign with an injected wallet.
| Method | What it does |
|---|---|
storeFile(opts) | Full pipeline: commit, approve + createDeal, wait for acceptance, upload to the provider, wait for activation. |
storeReplicated(opts) | Like storeFile with { replicas: n }: creates n deals for the same commitment with n distinct active providers (cheapest first), uploads to each, returns all deal ids. |
createDeal(opts) | Commit the data, ensure allowance, escrow KEP and create the deal on-chain. Returns the dealId from the DealCreated event. |
getDeal(dealId) | Read a deal's full on-chain state (parties, commitment, timestamps, escrow, faults, status). |
retrieve(dealId) | Fetch the bytes from the provider and verify size + Merkle root against the on-chain commitment before returning them. |
listProviders() | Enumerate the registry: endpoint, stake, capacity, usage, price, active flag. |
challenge(dealId) | Approve the bond and open a possession challenge; returns the challenged leaf index and deadline. |
submitProof(dealId, data) | Rebuild the tree from the original bytes and answer the open challenge with the right chunk + proof. |
reportFault(dealId) | Report an expired unanswered challenge, slashes the provider. |
withdrawEarnings(dealId) | Provider: pull the linearly vested escrow. |
settleDeal(dealId) / cancelDeal(dealId) | Close out an ended deal, or unwind one that never activated. |
registerProvider(opts) | Stake minStake and register an endpoint, capacity and price. |
faucet() / balance(addr?) | Claim testnet KEP; read any KEP balance. |
| Value | Name | Meaning |
|---|---|---|
| 0 | None | No deal at this id. |
| 1 | Created | Escrowed by the client, awaiting provider acceptance. |
| 2 | Accepted | Provider locked collateral; awaiting data upload + activation. |
| 3 | Active | Storage clock running; escrow vesting; challengeable. |
| 4 | Completed | Term ended and settled, provider paid, collateral returned. |
| 5 | Terminated | Ended early at 3 faults in the client's favor. |
| 6 | Cancelled | Unwound before activation; escrow refunded. |
| Command | What it does |
|---|---|
kepto providers | List registered storage providers. |
kepto store <file> --provider 0x… --duration 30d --price 5 | Create a deal, wait for acceptance, upload, wait for activation. Pass --replicas n to store the same commitment with n distinct providers (cheapest first). |
kepto status <dealId> | Deal status, vesting and any open challenge. |
kepto get <dealId> -o out.bin | Retrieve + verify against the on-chain root. |
kepto challenge <dealId> | Post a bond, demand a proof. |
kepto prove <dealId> <file> | Answer an open challenge from the original file. |
kepto fault <dealId> | Report an expired challenge. |
kepto withdraw <dealId> | Provider: withdraw vested earnings. |
kepto settle <dealId> | Settle an ended deal. |
kepto cancel <dealId> | Cancel a never-activated deal. |
kepto faucet | Claim 1,000 testnet KEP. |
kepto balance [address] | Show a KEP balance. |
kepto register-provider --endpoint https://… | Stake and register the signer as a provider. |
| Variable | Meaning |
|---|---|
KEPTO_CHAIN | local | robinhoodTestnet | robinhoodMainnet (default local). |
KEPTO_PRIVATE_KEY | Signer key for commands that send transactions. |
KEPTO_RPC | Override the chain's default RPC URL. |
KEPTO_TOKEN | KeptoToken address override. |
KEPTO_REGISTRY | ProviderRegistry address override. |
KEPTO_MARKET | StorageMarket address override. |
kepto store walks the same five steps the Store page performs in the browser: approve, create, wait for acceptance, upload, wait for activation.
A provider is an HTTPS server with a wallet. Registration and collateral live on-chain; the data plane is a small daemon (packages/provider-node) that stores raw files under a data directory and answers challenges automatically.
register(endpoint, capacityBytes, pricePerGiBPerEpoch). The endpoint must be your node's public HTTPS base URL.updateInfo, topUpStake, deactivate / reactivate and withdrawStake (deactivated, zero active deals, 7-day delay).Configure the daemon via environment variables:
| Variable | Meaning |
|---|---|
KEPTO_RPC | Chain RPC URL. |
KEPTO_PRIVATE_KEY | The provider wallet key (holds ETH for gas). |
KEPTO_MARKET | StorageMarket contract address. |
KEPTO_TOKEN | KeptoToken contract address. |
KEPTO_REGISTRY | ProviderRegistry contract address. |
State lives under a data dir: <dataDir>/content/<merkleRoot> (raw files) and <dataDir>/deals.json (manifest). See the package README for the full flag list.
DealCreated events directed at it and auto-accepts per policy (minimum price, max size, capacity headroom).activateDeal.ChallengeIssued and answers immediately with submitProof.Base URL = the registry endpoint. Errors are JSON { "error": string }; CORS allows all origins.
| Endpoint | Behavior |
|---|---|
GET /v1/info | Node metadata: { provider, chainId, market, capacityBytes, usedBytes, autoAccept, version }. |
PUT /v1/deals/:dealId/data | Raw file bytes (application/octet-stream). Verifies the commitment against the on-chain deal, persists chunks, activates the deal. Returns { dealId, merkleRoot, activated: true }. |
GET /v1/content/:merkleRoot | Streams the full file back (octet-stream). |
GET /v1/deals/:dealId | Local + on-chain deal status. |
The app is currently configured for Robinhood Chain Testnet (chain id 46630, NEXT_PUBLIC_CHAIN=robinhoodTestnet). Addresses resolve from explicit environment overrides first, then the generated deployment table in @kepto/core (getDeployment(chainId)).
| Contract | Address |
|---|---|
KeptoToken (KEP) | 0x5acA4749324507cff3530Ae34d7e6dbA0014Cff0 |
ComplianceRegistry | 0xD4320542E271Cf4B3e0A7b6A12e726209342f59B |
EquityVault | 0xfc57f94381b9Dfe2692695300aB1cB72bE6FeAB3 |
EquityToken (dAAPL) | 0x0849B18B89fb8A7E4080fF03493a0E811Cb8852a |
EquityToken (dTSLA) | 0x091cc510d2aCca7Ff1f70124894c308bd37060AA |
RwaRegistry | 0xaEF7722BE03A00B79dfa0a3012b3Aa7B51C0e9bf |
RwaVault | 0x30d7d4AeC7227D9Ee7F61fe80FCA3e3533cF8f80 |
StorageMarket | 0x2E6F2d7497A55a93efFfc832EF92855d9cB10833 |
ProviderRegistry | 0x65550FdcEb6E95Dd82477acD1605fc0bbAA8cA33 |
NEXT_PUBLIC_CHAIN=robinhoodTestnet
NEXT_PUBLIC_TOKEN_ADDRESS=0x…
NEXT_PUBLIC_REGISTRY_ADDRESS=0x…
NEXT_PUBLIC_MARKET_ADDRESS=0x…
NEXT_PUBLIC_RWA_REGISTRY_ADDRESS=0x…
NEXT_PUBLIC_RWA_VAULT_ADDRESS=0x…
NEXT_PUBLIC_EQUITY_VAULT_ADDRESS=0x…Rebuild after changing these, NEXT_PUBLIC_* variables are inlined at build time.
No. Providers store your raw bytes and anyone who knows the Merkle root can fetch them. If contents are sensitive, encrypt client-side before storing, the protocol proves possession, not confidentiality.
Challenge them. Every unanswered challenge slashes collateral and pays you and the challenger; three faults terminate the deal, refunding all unvested escrow plus remaining collateral to the client.
Your escrow is never at risk before activation. Cancel a Created deal any time; cancel an Accepted deal once the 24-hour activation window lapses. Full refund either way.
Only by breaking keccak256. The challenged index is derived from a block hash fixed after the challenge is committed, which the provider can't choose or predict, and the proof must reconstruct the exact committed root, including the domain-tagged leaf hash of the real chunk bytes.
Trust-minimization end to end: the app re-chunks the downloaded bytes, rebuilds the Merkle root and compares it to the on-chain commitment before saving the file. A tampered or truncated download simply fails the check.
No, Kepto is an experimental, unaudited protocol deployed on a testnet. Expect redeployments and resets. Don't use real funds; don't store the only copy of anything.