Retainer · for judges · 30 seconds
Your agent’s access renews itself on-chain at 3am, with nobody awake.
An x402-gated metered data feed on Hedera whose access window is an on-chain subscription that the Hedera Schedule Service (HIP-1215, system contract 0x16b) extends by itself. The agent signs exactly one thing — the first payment. Nothing signs anything again.
Nothing on this page needs an account, a key, or a clone. Every command below runs against the live deployment and the real Hedera testnet. There is no offline, mock or demo mode for the product.
The 30-second path
Watch a cold agent get charged.
curl -i "https://retainer.edycu.dev/api/retainer/access?agent=0x0000000000000000000000000000000000000abc"You get 402 Payment Required with a real x402 challenge —
scheme: exact,network: hedera:testnet, native HBAR — in the body and verbatim in thePAYMENT-REQUIREDheader, so an ordinary x402 client can parse it. That is the gate refusing service.Read an agent that has paid before.
curl -s "https://retainer.edycu.dev/api/retainer/status?agent=0xD14CA86A1483e9b2147a7B86fB74D437d3d2Cc66"No 402. Whatever the chain says about this agent right now — window open, or lapsed with
balanceTinybar: "0"after its last run ended — the balance, the metered allowance and the address of any pending scheduled renewal come straight off the contract. Nothing here is served from a database, and nothing is made to look alive.Check that on Hedera yourself, not on our word.
- The contract the server just read: 0.0.10415845 —
0x433050c9bd203FBdd49FAB6b5E20eD3E1FB2a931 - One renewal the network executed on its own —
CONTRACTCALL,scheduled=true,SUCCESS: 1788844334.069565823. No transaction was sent to trigger it. It is the first of eight in a row; all eight, and the one ordinary call that armed the first, come back from one mirror-node request:
curl -s "https://testnet.mirrornode.hedera.com/api/v1/transactions/0.0.7314364-1788844238-651641588" \ | jq -r '.transactions[] | [.consensus_timestamp, .name, "scheduled=\(.scheduled)", .result, "fee=\(.charged_tx_fee)"] | @tsv'- The contract the server just read: 0.0.10415845 —
Open the live view.
https://retainer.edycu.dev — paste an agent address and watch the window count down and then jump back up on its own. Every number on it is a chain read via
/api/retainer/status.
That is the whole product. Steps 1 and 2 are the two halves of the claim; step 3 is the part nobody has to trust us for.
The receipt block
Real numbers from real runs, not estimates. Every one is re-verifiable against the public Hedera mirror node — the exact curl commands are in docs/proof.md.
| x402 payment settled through Blocky402 | 0.0.7162784@1788840225.936068496 — 3 HBAR, agent → seller, fee paid by the facilitator; it opened the current deployment’s subscription |
|---|---|
| Unattended renewals executed by the network | 19 across three deployments (3 · 7 · 9), every one CONTRACTCALL scheduled=true SUCCESS with a Renewed event. One further scheduled execution reverted — limitation 3. |
| Cost of one self-re-arming renewal | 1.54896 HBAR on the first deployment; 1.54036 HBAR on the current one |
| Cost of a renewal that does not re-arm | 0.0507 HBAR on the first deployment; 0.0522 HBAR on the current one |
| What that ~30× gap proves | re-arming — the scheduleCall into 0x16b — is ~97% of what a renewal costs, on both deployments |
| Gas used, subscribe() on testnet | 1,582,554 (limit 2,000,000) |
| Gas used, deploy | 968,564 |
| Contract tests | 46 passing — yarn hardhat:test |
| Resource-server unit tests | 10 passing — yarn next:test |
| Amounts checked across the unit boundary | 202,059, three invariants each = 606,177 assertions |
| Hedera Schedule Service methods used | 3, all load-bearing: scheduleCall, hasScheduleCapacity, deleteSchedule |
The number worth ten seconds of attention is the 1.54896-vs-0.0507 split. It is the same function executing twice, and it is the whole cost story of unattended on-chain renewal — see docs/gas-economics.md.
Reproduce it
The real path — this is the product
Hits the live server and the real chain. It needs a funded ECDSA Hedera testnet account in ~/.config/retainer/hedera.env (BUYER_PRIVATE_KEY, BUYER_ACCOUNT_ID); credentials never live in the repo.
git clone https://github.com/edycutjong/retainer.git && cd retainer && yarn install
cd packages/nextjs
BASE_URL=https://retainer.edycu.dev yarn tsx scripts/retainer-agent.tsCold request → 402 → pay once over x402 → the server forwards that settled payment into subscribeFor → the same request again, now 200 with paidThisRequest:false → wait past expiry sending nothing → 200 again. The last step is the claim. A recorded run of exactly this is at the end of docs/proof.md.
The deterministic replay — CI only, never the demo
This does not exercise the Schedule Service; it runs against MockScheduleService.sol, because a Hardhat node has no system contract at 0x16b. It proves the contract logic, not the network behaviour.
yarn hardhat:test # 46 contract tests
yarn next:test # 10 unit tests, 202,059 amounts across the unit boundaryHonest limitations
Three real ones. None of them is fixed here.
- At the default price, Retainer loses money on every renewal. A renewal burns ~1.55 HBAR of the seller’s gas reserve to collect 1 HBAR of revenue. That is not a bug in the code — it is the actual economics of on-chain self-renewal at this gas limit, and pricing a period above the renewal cost is a product decision this build did not make.
- The metering write is fire-and-forget. The allowance is enforced by simulating
meter()against current chain state, but the recording transaction is not awaited — waiting put Hedera finality inside a serverless request and timed it out. A burst of requests arriving within the same few seconds can therefore overshoot the allowance by roughly the number in flight. Bounded, small, and disclosed rather than discovered. - One scheduled renewal on the deployed source reverted. At 1788840415.078121802 the network fired
renew()on0.0.10415845and the call came backCONTRACT_REVERT_EXECUTEDwith the contract’s ownInsolvent()guard — the check that the three money pots never exceedaddress(this).balance. The account’s real balance at that second, reconstructed from the mirror node, was 2,139,131,760 tinybar against pots totalling 1,900,000,000, so the balance the EVM exposed during the scheduled execution was lower than the account’s — consistent with Hedera reserving the call’s full gas cost on the payer before it runs. The subscription was restarted by hand (creditFor, then one ordinaryrenew()) and the network then executed eight renewals unattended to a loud lapse. The guard is right to exist and wrong to count that reservation; the fix needs a redeploy and is not made here. Three deployments exist —0.0.10406083,0.0.10414167,0.0.10415845— and docs/proof.md keeps them apart.
Also true: not audited, testnet only, and RENEWAL_COST_ESTIMATE is an explicit estimate — a contract cannot know a future network fee.
Links
| Live app | https://retainer.edycu.dev |
|---|---|
| Repository | https://github.com/edycutjong/retainer |
| Contract on HashScan | https://hashscan.io/testnet/contract/0.0.10415845 |
| On-chain proof, with re-verify commands | https://github.com/edycutjong/retainer/blob/main/docs/proof.md |
| What an unattended renewal costs | https://github.com/edycutjong/retainer/blob/main/docs/gas-economics.md |
| The unit trap, measured | https://github.com/edycutjong/retainer/blob/main/docs/hedera-units.md |
| Architecture | https://github.com/edycutjong/retainer/blob/main/specs/architecture.md |
| How AI was used, per file | https://github.com/edycutjong/retainer/blob/main/AI-USAGE.md |
| Security properties, each next to its test | https://github.com/edycutjong/retainer/blob/main/.github/SECURITY.md |