Documentation

Build on Kepto

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.

Overview

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.

The three pillars#

PillarWhat it isBacked by
Tokenized stocksERC-20 shares of real companies (dAAPL, dTSLA) you can trade 24/7.Attested reserves, capped in-contract by Secure Mint.
Real-world assetsAsset NFTs for property, invoices and bonds, fractionalizable into shares.Documents held under provable on-chain custody.
Verifiable storageMerkle-committed storage deals with challenges and slashing.Provider collateral and economic proofs of possession.

Actors#

ActorRole
Holder / investorMints, trades, stakes and redeems tokenized shares, or stores and verifies files. Settles in KEP.
Issuer / operatorAttests reserves and NAV, fills mint and redeem orders against off-chain custody, distributes dividends.
ProviderStores data. Stakes 1,000 KEP with a public endpoint, locks per-deal collateral, answers storage challenges.
ChallengerAnyone. Posts a 10 KEP bond to force a proof of possession, and is paid from slashed collateral if it fails.

Core contracts#

ContractPurpose
KeptoTokenERC-20 KEP settlement token, 18 decimals, testnet faucet.
EquityTokenTokenized stock: reserves, Secure Mint, mint/redeem queue, NAV, KEP dividends.
ComplianceRegistryAllowlist / blacklist, freeze and exempt, gates share transfers.
EquityVaultStakes shares into position NFTs and streams KEP emission APR plus dividends.
RwaRegistry / RwaVaultAssets backed by storage deals, attestation, and fractionalization.
StorageMarket / ProviderRegistryDeals, escrow vesting, challenges, proofs, slashing.

Ways to integrate#

SurfaceBest for
dAppHumans with a browser wallet: trade, stake, store, verify, interactively.
HTTP APIServices and scripts: a hosted gateway turns plain HTTP into on-chain deals.
SDK & CLITypeScript apps and terminals: full programmatic control of every flow.
!Heads up
Kepto is experimental, unaudited testnet software. Custody, legal title and KYC truthfulness are trusted off-chain; the chain enforces the supply, transfer, proof and dividend mechanics. Do not use real funds or store irreplaceable data.

Tokenized stocks

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.

Secure Mint and 1:1 backing#

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.

Tip
The reserves figure is an on-chain attestation of off-chain custody. The chain guarantees supply never exceeds the attested number; a custodian and auditor are still trusted to keep that number honest.

Compliance-gated transfers#

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.

Mint and redeem#

Primary issuance uses an on-chain order queue so KEP and shares are only ever escrowed by the contract, never by a person.

  1. 1
    Request
    You call requestMint(paymentKep); the contract escrows your KEP and records a pending order. Redeeming is the mirror, requestRedeem(shares) escrows your shares.
  2. 2
    Fill
    The operator fills the order against real custody: 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.
  3. 3
    Or cancel
    While an order is still pending you can cancelOrder(id) and get your escrow back to the wei.

NAV and dividends#

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.

On-chain versus off-chain#

Enforced on-chainTrusted off-chain
Supply can never exceed attested reservesReal custody and legal title of the shares
Transfer restrictions and freezesTruthfulness of the reserves attestation
Mint and redeem authorization and escrowBroker-dealer and transfer-agent licensing
Dividend split and claim mathKYC verification of holders

Try it#

kepto CLI
# 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>

Staking and APR

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.

StreamWhere it comes from
KEP emissionsA funded reward pool per stock streams KEP to stakers per second, Synthetix-style, over a set duration.
Dividend passthroughDividends the staked shares accrue are harvested by the vault and forwarded to stakers pro rata, so staking never costs you dividend rights.

How APR is computed#

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.

Yield-bearing NFTs#

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.

iNote
Emissions run for a funded period and stop at 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.

Try it#

kepto CLI
# 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>

How storage deals work

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)
                 Cancelled

1, Create

The 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.

2, Accept

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.

3, Activate

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.

4, Earn, settle, or unwind

  • Vesting, escrow vests linearly: vested = totalPrice · (min(now, endTime) − startTime) / duration. The provider can pull the vested balance at any time with withdrawEarnings.
  • Settlement, after 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.
  • Cancellation, the client can 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.
  • Termination, three unanswered challenges end the deal in the client's favor (see the slashing model below).

Protocol parameters

ParameterDefaultMeaning
acceptWindow24 hProvider must accept within this window after creation.
activationWindow24 hProvider must activate within this window after accepting.
collateralBps5,000 (50%)Collateral as a share of the deal price.
minDealDuration1 dayShortest allowed term.
maxDealDuration360 daysLongest allowed term.
challengeBond10 KEPBond posted to open a challenge.
challengeCooldown12 hMinimum spacing between challenges per deal.
proofWindow8 hProvider's deadline to answer a challenge.
maxFaults3Faults that terminate the deal.

Proof & slashing model

Storage without proofs is a promise. Kepto makes the promise expensive to break with a challenge game anyone can play.

Challenges

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 leafCount

Because 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.

Proofs

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.

Faults & slashing

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).
  • Half of the slash goes to the challenger (plus their bond back); half goes to the client.
  • At 3 faults the deal terminates: the client receives all unwithdrawn escrow plus the remaining collateral. The provider keeps only what it already withdrew.
iNote
The incentive lattice: providers answer because silence costs collateral; watchtowers challenge because faults pay; clients sleep because either the data is provably there, or they get paid.

Chunking & Merkle spec

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.

Constants & rules

RuleDefinition
CHUNK_SIZE4096 bytes. The final chunk may be shorter (1..4096). Zero-length files are not storable.
leafCountceil(size / 4096), and it must hold that (size + 4095) / 4096 == leafCount.
Leaf hashkeccak256(0x00 ‖ chunk_bytes), a single 0x00 domain tag prepended to the raw chunk bytes.
Node hashkeccak256(0x01 ‖ left ‖ right), a single 0x01 tag, then the two 32-byte child hashes.
Odd levelsWhen a level has an odd number of nodes, the last node is promoted unchanged to the next level, no duplication.
Single chunkThe merkleRoot of a single-chunk file is its leaf hash.

Proof format

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 == root

Every proof element must be consumed, over-long proofs are invalid, which rules out proof-padding games.

Reference implementations

LanguageLocation
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.

RWA registry

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.

The flow

  • Register, 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, an approved attestor (owner-managed allowlist) calls 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.
  • Valuation, any attestor can revise declaredValue with updateValuation(assetId, newValue), emitting ValuationUpdated with the old and new values.
  • Lapse, anyone can call 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.
  • Renew, the asset owner repairs backing with 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, 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.

Attestation quorum and expiry

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.
  • Once the validity window has passed, anyone can call flagStale(assetId): the asset returns to Registered with all approvals reset (AttestationExpired), and a fresh quorum is required.
  • Approvals also reset whenever the evidence changes: on flagLapsed, on addDocument, and on a renewDocument that restores a Lapsed asset.

Income distribution

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.
  • Earned income survives share transfers and the burn in redeem: selling or redeeming shares never forfeits income already earned, and a buyer does not inherit the seller's past distributions.
  • Integer rounding can strand up to a few wei of dust per distribution in the token, amounts always round down in the holder's disfavor.

Statuses

ValueStatusMeaning
0NoneNo asset at this id.
1RegisteredMinted with documents bound to Active deals; awaiting attestation.
2AttestedThe attestor quorum verified the asset against the stored documents. Unlocks fractionalization while the attestation is current.
3LapsedSome backing deal is no longer Active. Attestation is lost; renewing every broken document returns it to Registered.
4BannedAdmin-terminal state for fraudulent assets. No way back.

Registration fee

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.

The renewal convention

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.

iNote
The registry never sees the documents, only their Merkle roots. What makes the paperwork trustworthy is the storage layer underneath: random challenges force providers to keep every chunk of every document retrievable, on pain of slashing.

Staked equities

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, 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.
  • Attach, 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, 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.

Equity CLI

CommandWhat it does
kepto equity stocksList 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 positionsList 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.

Equity gateway endpoints

The gateway exposes the vault read-only under /api/v1/equity:

MethodPathPurpose
GET/api/v1/equity/stocksListed equity tokens with symbol, address and total locked.
GET/api/v1/equity/positionsOpen positions with stock, amount, opener and owner.
GET/api/v1/equity/positions/:idOne position with its attached documents.
iNote
Collateral proof here is fully on-chain: the vault holds the tokens, so no attestation is needed for the backing. The equity tokens themselves are issued by their issuer, the vault does not mint equity exposure, it locks and receipts what already exists.

HTTP API

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.

Base URL & authentication

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 }.

Endpoints

MethodPathPurpose
GET/api/v1/statusService metadata: chain, contract addresses, gateway wallet and its KEP balance. No auth.
GET/api/v1/providersRegistered storage providers with endpoint, capacity, usage and pricing.
POST/api/v1/filesStore 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/:dealIdDeal status for a stored file, including vested KEP and any open challenge.
GET/api/v1/files/:dealId/contentRetrieve 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/dealsAll deals created by the gateway wallet.
POST/api/v1/deals/:dealId/challengePost the bond and challenge the provider to prove possession.
POST/api/v1/deals/:dealId/settleSettle an ended deal, pays the provider, returns collateral.
POST/api/v1/deals/:dealId/faultReport an expired, unanswered challenge (slashes the provider).
GET/api/v1/openapi.yamlThe full OpenAPI 3.0 contract. No auth.

Store a file

curl
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.tar

The call blocks through the whole pipeline, commit, escrow, provider acceptance, upload, activation, and returns the live deal:

201 Created
{
  "dealId": 42,
  "provider": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
  "merkleRoot": "0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658",
  "sizeBytes": 1048576,
  "leafCount": 256,
  "status": "Active",
  "durationSeconds": 2592000,
  "totalPriceKep": "5",
  "collateralKep": "2.5",
  "filename": "backup.tar"
}

Check status

curl
curl -H "Authorization: Bearer $API_KEY" "$GATEWAY/api/v1/files/42"

Replication

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.

Free-tier quotas

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.

Retrieve verified bytes

curl
curl -H "Authorization: Bearer $API_KEY" \
  -o backup.tar "$GATEWAY/api/v1/files/42/content"

Run your own gateway

The gateway is a single Node process (npm run start -w @kepto/gateway) configured entirely via environment variables:

VariableMeaning
GATEWAY_PRIVATE_KEYRequired. The gateway wallet key, holds ETH for gas and KEP for escrows.
KEPTO_CHAINlocal | robinhoodTestnet | robinhoodMainnet (default local).
API_KEYSComma-separated bearer tokens. Empty = open access.
PORTListen port (default 8748).
DEFAULT_DURATION_SECONDSDeal duration when ?duration is omitted (default 30 days).
DEFAULT_PRICE_LUM_PER_GIB_30DPrice basis in KEP per GiB per 30 days used when ?price is omitted (default 10).
FREE_TIER_MAX_BYTESCumulative upload cap per API key in bytes; 0 = unlimited (default). Requires API_KEYS.
iNote
The gateway signs with its own wallet, API keys are the only thing standing between the internet and its KEP balance. Set API_KEYS for anything reachable from outside localhost.

SDK & CLI

@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.

Quickstart

store-and-retrieve.ts
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.

Key methods

MethodWhat 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.

DealStatus

ValueNameMeaning
0NoneNo deal at this id.
1CreatedEscrowed by the client, awaiting provider acceptance.
2AcceptedProvider locked collateral; awaiting data upload + activation.
3ActiveStorage clock running; escrow vesting; challengeable.
4CompletedTerm ended and settled, provider paid, collateral returned.
5TerminatedEnded early at 3 faults in the client's favor.
6CancelledUnwound before activation; escrow refunded.

The kepto CLI

CommandWhat it does
kepto providersList registered storage providers.
kepto store <file> --provider 0x… --duration 30d --price 5Create 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.binRetrieve + 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 faucetClaim 1,000 testnet KEP.
kepto balance [address]Show a KEP balance.
kepto register-provider --endpoint https://…Stake and register the signer as a provider.

CLI configuration

VariableMeaning
KEPTO_CHAINlocal | robinhoodTestnet | robinhoodMainnet (default local).
KEPTO_PRIVATE_KEYSigner key for commands that send transactions.
KEPTO_RPCOverride the chain's default RPC URL.
KEPTO_TOKENKeptoToken address override.
KEPTO_REGISTRYProviderRegistry address override.
KEPTO_MARKETStorageMarket 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.

Provider guide

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.

1, Fund & register

  • Get testnet ETH (gas) and at least 1,000 KEP (stake), see the faucets.
  • Register on the Providers page (or via CLI): approve the stake, then register(endpoint, capacityBytes, pricePerGiBPerEpoch). The endpoint must be your node's public HTTPS base URL.
  • Manage the listing later with updateInfo, topUpStake, deactivate / reactivate and withdrawStake (deactivated, zero active deals, 7-day delay).

2, Run the node

Configure the daemon via environment variables:

VariableMeaning
KEPTO_RPCChain RPC URL.
KEPTO_PRIVATE_KEYThe provider wallet key (holds ETH for gas).
KEPTO_MARKETStorageMarket contract address.
KEPTO_TOKENKeptoToken contract address.
KEPTO_REGISTRYProviderRegistry 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.

3, What the daemon does

  • Watches DealCreated events directed at it and auto-accepts per policy (minimum price, max size, capacity headroom).
  • Verifies uploads: re-chunks the body, recomputes the root, then calls activateDeal.
  • Watches ChallengeIssued and answers immediately with submitProof.
  • Withdraws vested earnings and settles ended deals on a timer.

Provider HTTP API

Base URL = the registry endpoint. Errors are JSON { "error": string }; CORS allows all origins.

EndpointBehavior
GET /v1/infoNode metadata: { provider, chainId, market, capacityBytes, usedBytes, autoAccept, version }.
PUT /v1/deals/:dealId/dataRaw 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/:merkleRootStreams the full file back (octet-stream).
GET /v1/deals/:dealIdLocal + on-chain deal status.

Contract addresses

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)).

ContractAddress
KeptoToken (KEP)0x5acA4749324507cff3530Ae34d7e6dbA0014Cff0
ComplianceRegistry0xD4320542E271Cf4B3e0A7b6A12e726209342f59B
EquityVault0xfc57f94381b9Dfe2692695300aB1cB72bE6FeAB3
EquityToken (dAAPL)0x0849B18B89fb8A7E4080fF03493a0E811Cb8852a
EquityToken (dTSLA)0x091cc510d2aCca7Ff1f70124894c308bd37060AA
RwaRegistry0xaEF7722BE03A00B79dfa0a3012b3Aa7B51C0e9bf
RwaVault0x30d7d4AeC7227D9Ee7F61fe80FCA3e3533cF8f80
StorageMarket0x2E6F2d7497A55a93efFfc832EF92855d9cB10833
ProviderRegistry0x65550FdcEb6E95Dd82477acD1605fc0bbAA8cA33

Overriding

.env.local
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.

FAQ

Is my data private?

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.

What if the provider goes offline?

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.

What if the provider never accepts or activates my deal?

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.

Can a provider fake a proof?

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.

Why does retrieval verify in my browser?

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.

Is this production-ready?

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.