# copy the API key from the service page or /passes
bun call:service <serviceId> "<passKey>.<secret>" getQuote '{"symbol":"BTC"}'
curl -s http://localhost:3000/api/gateway/<serviceId> \
-H 'authorization: Bearer <passKey>.<secret>' \
-H 'content-type: application/json' \
-d '{"operation":"getQuote","input":{"symbol":"BTC"}}'Documentation
Choose a topic to get started.
Overview
Providers publish APIs. Clients buy timed access with USDC and call APIs using an API key for each pass.
Network references
How it works
Publish a listing, purchase access, then call the API.
Publish
provider- 1form → manifest (operations, typed inputs)
- 2uploadData → Swarm → manifestRef
- 3optional private file → ACT upload
- 4POST /api/services → Arkiv service entity
Buy
client- 1approve USDC → buy() on the contract
- 2server verifies the Purchased event
- 3browser generates a pass secret
- 4POST /api/access-passes → access_pass + sale on Arkiv
Call
client or agent- 1Authorization: Bearer <passKey>.<secret>
- 2gateway reads the pass on Arkiv
- 3checks service, expiry, keccak(secret)
- 4forwards or answers with the demo bot
Access pass
Your API key combines a public pass ID with a private secret. Access ends when the pass expires.
| Piece | Where | Who can read it |
|---|---|---|
| passKey | Arkiv entity key | everyone (explorer) |
| secret | generated in the buyer's browser | buyer only |
| secret_hash | Arkiv attribute · keccak256(secret) | everyone · useless without the secret |
| encryptedSecret | Arkiv payload · AES-256-GCM under a key derived from the buyer's Swarm ID | buyer, on any device |
| API key | <passKey>.<secret> | whoever the buyer gives it to, until expiry |
| Request | Response |
|---|---|
| no header | 401 · Missing access pass |
| passKey only | 401 · Missing pass secret |
| wrong secret | 403 · Pass secret does not match this pass |
| other service | 403 · This pass is for a different service |
| after expiry | 403 · Access pass not found on Arkiv or expired |
| valid | 200 · result + verification (block, seconds left) |
Private files
Providers can include one encrypted file, up to 512 KB. Buyers need a separate grant from the provider to download it.
- 1
Publish with a file
Open Private file in the publish form.actUploadDatawith an empty grantee list. Arkiv stores name, size, type and the three ACT references. - 2
Buyer purchases
The pass and the sale record the buyer's Swarm sharing key (buyer_pubkey). - 3
Provider grants
My APIs, File access, Grant access.actAddGranteesruns in the provider's browser;POST /api/grantsrecords the new history reference as agrantentity. - 4
Buyer downloads
Service page, "Private file", Download.actDownloadDatadecrypts with the buyer's own Swarm ID. No shared key ever travels.Expect · the file opens; a non-granted identity gets a decryption error
Data on Arkiv
Listings and receipts are public. Pass secrets and private files stay encrypted.
| Entity | Attributes | Payload | Lifetime |
|---|---|---|---|
| service | app, entity_type, service_id, category, provider_id, provider_name, available, version, manifest_ref, price_usdc, access_seconds, payout_address (+ private_* when a file is attached) | name, description | permanent |
| access_pass | service_id, provider_id, buyer_id, buyer_address, buyer_pubkey, tx_hash, paid_usdc, chain_id, secret_hash | serviceName, purchasedAt, encryptedSecret | expires after access_seconds |
| sale | same as the pass minus secret_hash, plus pass_key | serviceName, purchasedAt | permanent |
| grant | service_id, provider_id, buyer_id, buyer_pubkey, act_history_ref, act_enc_ref, act_pubkey | grantedAt | permanent |
one entity $key = key(0x<entity key>)
all app entities $owner = addr(0x401629d4c1A4C1A0Ffd14A089f798Dd29A94c09C)
open https://data.arkiv.network/?chain=tiramisuAPI
Six endpoints. Payments and pass secrets are verified before access is granted.
| Route | Does | Trust |
|---|---|---|
| POST /api/services | creates the service entity (after the manifest is on Swarm) | providerId from the client is trusted |
| POST /api/access-passes | verifies the Fuji payment (Purchased event or USDC transfer), refuses reused tx hashes, mints access_pass + sale | buyer must be the payer |
| POST /api/grants | records an ACT grant made in the provider's browser | providerId must own the service |
| POST /api/bot/[serviceId] | demo bot behind a pass · getQuote returns a live price | pass + secret verified on Arkiv |
| POST /api/gateway/[serviceId] | verifies the pass, forwards to the provider endpoint or answers with the bot | pass + secret verified on Arkiv |
| GET /api/services | writer health: address, GLM balance, funded | public |
Run it
Bun 1.3+, Node.js 20+, and test funds on Avalanche Fuji. Foundry is optional for contract tests.
git clone https://github.com/pf55351/apiritivo-eth-26 && cd apiritivo-eth-26
bun install
cp .env.example apps/web/.env.local # writer key, gateways, contract address pre-filled
bun demo:check # must end with READY
bun dev # http://localhost:3000
bun typecheck && bun lint && bun test # TypeScript, ESLint, unit tests
bun test:contracts # Foundry tests
bun run build # stop bun dev first: both write apps/web/.next| Faucet | For | Link |
|---|---|---|
| GLM · Arkiv | the app writer 0x401629… (only if "writer unfunded" appears) | hub.arkiv.network/faucet |
| AVAX · Fuji | gas for the buyer (approve + buy) and for a provider claim | core.app testnet faucet |
| USDC · Fuji | the buyer's payment wallet, MetaMask or Rabby (0.50 per pass; the faucet gives 10) | faucet.circle.com |
Judge walkthrough
Use separate browser profiles for Provider and Client. Each purchase needs test USDC and AVAX for gas.
- 1
Run the app
Expect · home page with Enter with Swarm ID
- 2
Create the provider identity
Enter with Swarm ID, click the button in the dialog, create a new identity in the popup, then choose Provider. On My APIs, then open your account menu in the header to check storage and the Arkiv writer.Expect · Storage ready and a funded writer
- 3
Publish an API
Publish API: name, category, price 0.50 USDC, duration 30 seconds (demo), one operationgetQuote(symbol: string). Payout wallet is your Swarm wallet, fixed. Optionally attach a small private file.Expect · success screen with Swarm gateway, Arkiv entity and transaction links; open both
- 4
Create and fund the buyer
Switch to Client: the app signs you out and asks for Swarm ID again. Sign in and open the service from the marketplace. Only the access card asks for MetaMask or Rabby on Avalanche Fuji (the app offers the network). Open Fund wallet and copy your address, fund it with AVAX and USDC from the faucets, click refresh.Expect · USDC ≥ 0.50 and some AVAX
- 5
Buy access
Buy access · 0.50 USDC. Approve, buy() on the contract, confirmation, mint on Arkiv. Two Fuji transactions signed by your wallet, then one signature that claims the pass for your Swarm ID, which seals the API key with its own key.Expect · Active access with expiry and a link to My passes; the service page keeps describing the API
- 6
Call the API
In My passes, under the pass: Try API withgetQuote,BTC; the private file, when the provider granted it, downloads there too. From a terminal:bun call:service <serviceId> "<passKey>.<secret>" getQuote '{"symbol":"ETH"}'. See Access pass for error responses.Expect · Pass verified on Arkiv with expiry block, a live price, 401/403 on every negative check
- 7
Private file (optional)
My APIs, File access, Grant access for the buyer. Buyer: service page, Private file, Download.Expect · the file decrypts for the granted buyer only
- 8
See the sale
My APIs: recorded sales and revenue update. Open Sales receipts for pass and payment links.Expect · the sale receipt opens in the Arkiv data explorer
- 9
Claim the money
Send a little AVAX to the provider's Swarm wallet. In Sales, tab Payment activity, select Claim USDC. Optional: withdraw USDC to any address, reveal the private key for MetaMask.Expect · USDC balance goes up, claim tx on the explorer
- 10
Verify without the app
Arkiv data explorer with$owner = addr(0x401629d4…); the contract's Purchased and Claimed events on the explorer; manifest bytes athttps://api.gateway.ethswarm.org/bytes/<manifest_ref>.Expect · every entity, event and byte reachable outside APIritivo
| Symptom | Fix |
|---|---|
| popup does not open | allow popups for this site, retry from the sign in dialog; Brave: use the button inside the dialog |
| Arkiv writer unfunded | faucet GLM to 0x401629d4…, refresh |
| Swarm upload unavailable | the app falls back to a direct gateway upload |
| Buy button disabled | USDC or AVAX still 0: wait for the faucet, click refresh |
| marketplace empty | Tiramisu is a testnet and may have been reset: publish again |
| Cannot find module .next/… | stop bun dev, delete apps/web/.next, start one bun dev |
| changed .env.local | restart bun dev: NEXT_PUBLIC_* is inlined at start |
Code map
SDK integrations live in packages. The web app uses shared types and adapters.
| Path | What |
|---|---|
| packages/swarm/src/index.ts | Swarm ID login, manifest upload/download, wallet + key derivation, drive, ACT private files |
| packages/arkiv/src/index.ts | typed Arkiv queries, block timing, verifyAccessPass, explorer links |
| packages/arkiv/src/pass-secret.ts | pass secret: keccak on-chain, AES-GCM for the buyer, bearer format |
| packages/arkiv/src/server.ts | publishService, issueAccessPass, publishGrant: the only writes |
| packages/payments/src/server.ts | verifyPayment: Purchased event or USDC Transfer, pure receipt checks |
| packages/payments/src/browser.ts | Swarm wallet and browser wallet signers, pass key signature, pay, claim, watchSales live feed |
| packages/ens/src/index.ts | read-only ENS: resolve addr, text record and bzz contenthash, verify a linked name |
| apps/web/lib/injected-wallet.tsx | client payment wallet: MetaMask / Rabby connection, network switch |
| contracts/src/APIritivoPayments.sol | buy / claim ledger, Foundry tests |
| apps/web/app/api/* | services, access-passes, grants, bot, gateway |
| tools/ | demo-check, call-service |
ETHRome sponsors
Official sponsors of ETHRome 2026, where APIritivo was built.


