Kepto documents a trade-only mainnet dAAPL synthetic tracker, separate Robinhood Chain Testnet equity simulations, real-world asset records, and verifiable storage. Mainnet dAAPL settles in KEP, does not custody Apple stock, and gives no shareholder, voting, dividend, staking, or guaranteed NAV redemption right.
Kepto puts public asset state and backing commitments on-chain so they can be inspected independently. It is three products on one settlement layer: equity markets with a mainnet synthetic trade path and separate testnet contract prototypes, real-world assets bound to active, challengeable storage-deal commitments, andverifiable storage with on-chain Merkle roots and economic spot checks. 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 recorded state is readable on-chain at any moment, not promised in a quarterly PDF. Supply is capped by the issuer's on-chain reserve figure, storage challenges provide economic spot checks, and retrieved bytes are re-hashed against an on-chain root before they reach you. These checks do not independently audit an offchain reserve or guarantee future file availability.
| Pillar | What it is | Verification boundary |
|---|---|---|
| Equity markets | Mainnet dAAPL synthetic trading, plus separate Robinhood Chain Testnet equity-token examples. | Oracle and market-health checks on mainnet; issuer-attested fields in the testnet prototype. |
| Real-world assets | Asset NFTs for property, invoices and bonds, fractionalizable into shares. | Document roots referenced by active, challengeable storage deals. |
| Verifiable storage | Merkle-committed storage deals with challenges and slashing. | Provider collateral and economic proofs of possession. |
| Actor | Role |
|---|---|
| Trader / testnet participant | Trades the mainnet dAAPL synthetic market, or tests prototype mint, stake and redeem flows on Robinhood Chain Testnet. Settles in KEP. |
| Issuer / operator | Administers the testnet reserve, NAV, mint, redeem and distribution contract fields. These are not mainnet dAAPL promises. |
| 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 | Testnet equity prototype: attested reserve cap, mint/redeem queue, NAV field and KEP distribution functions. |
ComplianceRegistry | Testnet allowlist / blacklist, freeze and exempt controls for equity-token transfers. |
EquityVault | Testnet-only prototype that locks equity tokens into position NFTs and can stream funded KEP emissions. |
RwaRegistry / RwaVault | Assets backed by storage deals, attestation, and fractionalization. |
StorageMarket / ProviderRegistry | Deals, escrow vesting, challenges, proofs, slashing. |
KeptoCcipPublisher / Receiver | Optional testnet-only typed snapshots from Robinhood Testnet to Arbitrum Sepolia. |
From an RWA, equity, or storage detail page, Publish cross-chain proof reviews the current public fields and a native ETH fee, then asks the source publisher to reread the canonical state during execution. Once verified publisher and receiver deployment records are configured, the resulting snapshot travels from Robinhood Chain Testnet through Chainlink CCIP to the authenticated receiver on Arbitrum Sepolia. Until then, the action fails closed. The preview is not the mined evidence, and snapshots are timestamped history rather than continuous sync. After a matching destination receipt is authenticated, these exact labels apply:
Chainlink transports and authenticates the cross-chain message. It does not verify legal title, custody, reserves, NAV or valuation, issuer attestations, documents, Robinhood-testnet equity prices, or future storage availability. Plaintext labels, URIs, documents, file bytes, keys, and private account data stay excluded, although hashes can still be correlated with public source state. Use Proofs for authenticated source events, CCIP message IDs, destination receipts, and updated snapshots.
KEP is the settlement and coordination token of Kepto, with a fixed maximum supply of 1,000,000,000 tokens. It is non-mintable — no supply can ever be created after launch. Allocation:
| Allocation | Share | Tokens |
|---|---|---|
| Airdrops | 20% | 200,000,000 |
| Staking | 20% | 200,000,000 |
| CEXs / Ecosystem | 20% | 200,000,000 |
| Rewards | 10% | 100,000,000 |
| LP (liquidity) | 10% | 100,000,000 |
| Marketing | 10% | 100,000,000 |
| Team | 5% | 50,000,000 |
| Strategic investors | 5% | 50,000,000 |
Totals to 100% of the fixed 1,000,000,000 KEP supply. The token contract has no mint function, no owner-controlled inflation, and standard transfers are untaxed.
Mainnet dAAPL is a KEP-settled synthetic tracker of an oracle-published AAPL value. It is trade-only: Kepto does not custody Apple stock, and dAAPL provides no shareholder, voting, dividend, staking, or guaranteed NAV redemption right.
The contracts below are separate Robinhood Chain Testnet prototypes. Their ERC-20 tokens model issuer-configured equity records for testing; they do not establish legal title to a company. The testnet deployment includes twelve EquityToken examples, from dAAPL and dTSLA through dPLTR and dSPY, priced and traded in KEP. Their contracts enforce the supply, transfer, settlement, and reserve-cap rules described below.
Each testnet token carries an on-chain reserves figure the issuer attests to. Every mint runs through Secure Mint, which reverts unless totalSupply() + amount <= reserves. Tokens above that attested cap cannot be issued, and reservesRatioBps() reports the testnet contract ratio for anyone to read.
Testnet transfers pass through a shared ComplianceRegistry. In blacklist mode (the current Robinhood Chain Testnet deployment default) tokens move freely except for frozen accounts, so the examples 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.
The testnet prototype uses an on-chain order queue so KEP and equity tokens are escrowed by the contract while an operator decides whether to fill an order.
requestMint(paymentKep); the contract escrows your KEP and records a pending order. Redeeming is the mirror, requestRedeem(amount) escrows your testnet equity tokens.fillMint issues tokens through Secure Mint and forwards KEP to the treasury; fillRedeem burns tokens and pays KEP. The contract does not guarantee that an operator will fill an order.cancelOrder(id) and get your escrow back to the wei.The testnet navPerShare field records an issuer-attested value per token. Its pull-model distribution functions can allocate funded KEP: 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. These testnet functions are not offered by the mainnet dAAPL trade-only release.
| Testnet contract enforces | Still depends on the issuer / operator |
|---|---|
| Supply cannot exceed the issuer-recorded reserve cap | Any off-chain custody and legal arrangement |
| Transfer restrictions and freezes | Truthfulness of the testnet reserve attestation |
| Mint and redeem authorization and escrow | Broker-dealer and transfer-agent licensing |
| Funded distribution and claim math | KYC verification of holders |
# read a testnet equity token: reserve cap, NAV field, 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 a funded testnet KEP distribution
KEPTO_PRIVATE_KEY=0x... kepto equity claim <dAAPL>This section describes the Robinhood Chain Testnet EquityVault prototype only. Mainnet dAAPL is trade-only and has no public staking or dividend promise. On testnet, the vault locks equity tokens and mints an ERC-721 position NFT that records the position.
| 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 | Funded testnet token distributions are harvested by the vault and forwarded to position holders pro rata. |
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 testnet receipts. earned(positionId) reports the configured reward and distribution, claimRewards(positionId) pays them out, and unstake(positionId) auto-claims everything and returns your exact locked testnet equity tokens.
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 commitments reference active Kepto storage deals. Every document record binds the asset to a deal ID and Merkle root. If a referenced deal is no longer live, anyone can flag that deal-status condition and move the asset to Lapsed; the registry does not observe missing file bytes directly.
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.
This section describes a Robinhood Chain Testnet prototype. Its document, reserve, and token records simulate contract mechanics; they do not prove company-share custody, legal ownership, or off-chain asset backing.
On testnet, EquityVault locks configured prototypeEquityToken units and issues a transferable ERC-721 position (Kepto Equity Position, IEQP). The contract can prove only that the recorded token amount remains in the vault. It cannot prove what an off-chain asset represents or who holds it. unstake burns the position and returns the same recorded token amount, with no fee or oracle.
Only owner-listed testnet tokens can enter the simulation (listStock / delistStock); delisting blocks new positions while existing ones unwind normally. The current testnet inventory uses the deployed EquityToken prototypes. Units enter wallets through simulated mint, order, or trading flows; these contracts do not expose a public token faucet.
stake(stockToken, amount, metadataURI) pulls testnet 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 recorded token amount. The vault does not establish off-chain equity exposure.Views expose the state on-chain: getPosition, getDocuments, getStock, stockCount / stockAt, positionCount and stockSymbol. This testnet simulation runs through the Equity CLI below, while the Equity page in the app surfaces the prototype state: an issuer-recorded reserve cap, compliance flags, simulated mint and redeem orders, and funded token-distribution events.
| Command | What it does |
|---|---|
kepto equity stocks | List the vault's configured testnet tokens with symbol, address and total locked. |
kepto equity stake <symbol|token> <amount> [--uri …] | Approve a configured testnet token and lock it, minting a position NFT. |
kepto equity positions | List open testnet positions with token, 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. |
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 is eligible, 413 for the per-file or lifetime account allowance, or 507 when the configured storage safety reserve would be crossed. |
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)). The cross-chain publisher and receiver resolve separately from generated getCcipDeployment(chainId) records; an unpublished record remains visibly marked as not configured and is never linked to a placeholder address.
| Contract | Network | Address |
|---|---|---|
KeptoToken (KEP) | Robinhood Chain Testnet | 0x5acA4749324507cff3530Ae34d7e6dbA0014Cff0 |
ComplianceRegistry | Robinhood Chain Testnet | 0xD4320542E271Cf4B3e0A7b6A12e726209342f59B |
EquityVault | Robinhood Chain Testnet | 0xfc57f94381b9Dfe2692695300aB1cB72bE6FeAB3 |
EquityToken (dAAPL) | Robinhood Chain Testnet | 0xf092981daff283ff4cec518cf10cb0e40bd778f9 |
EquityToken (dTSLA) | Robinhood Chain Testnet | 0x7918ac0c239f842ab0d82f253813afa0018cf19a |
RwaRegistry | Robinhood Chain Testnet | 0xc1f2EE45Dd9482560B75DF9722c95F90158550c6 |
RwaVault | Robinhood Chain Testnet | 0x4725e3605a51A3AA2ee8162c1307908e956944d6 |
StorageMarket | Robinhood Chain Testnet | 0xf3ca8339e77b74847F2208d95E6b0eB8775f766e |
ProviderRegistry | Robinhood Chain Testnet | 0x65550FdcEb6E95Dd82477acD1605fc0bbAA8cA33 |
KeptoCcipPublisher | Robinhood Chain Testnet | 0x0cF7484860e2444e0FFB7C7AaE1dD74D3043BBAb |
KeptoCcipReceiver | Arbitrum Sepolia | 0x7BD7F40cD0A2248be4C88C84578cd983C454A156 |
Standalone Chainlink CCIP proof deployed on Robinhood Chain mainnet (chain 4663) with an authenticated receiver on Arbitrum One (chain 42161). These are separate from the testnet app deployment above and are wired to the live KEP token.
| Contract | Network | Address |
|---|---|---|
KeptoCcipPublisher | Robinhood Chain | 0x8c5d7DB16225eD1039cAC027DeE74B1050bbd17c |
KeptoCcipReceiver | Arbitrum One | 0x78085400EDbB23849b7e2eCf9D9E026c9FEfa2E2 |
StorageMarket | Robinhood Chain | 0xb4E8b09F57c7CDEB7143b6c84ac0970807C6308f |
RwaRegistry | Robinhood Chain | 0x70f26b558f17eC5D431968Fcf00E9cfF12243919 |
EquityToken (attested) | Robinhood Chain | 0xb406486A3380Fb1614974d3aAa94d57a03B58af2 |
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.
Chainlink CCIP transports and authenticates the cross-chain message; Kepto's Arbitrum Sepolia receiver checks the exact source, publisher, typed payload, and state hash. That verifies a timestamped message and payload, not legal title, custody, reserves, NAV or valuation, issuer attestations, documents, prices, or future file availability. A source dispatch is not destination verification, and a delay is not failure without authoritative evidence.
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.