Your agent's access renews itself on-chain at 3am, with nobody awake.
An x402-gated resource on Hedera whose access window is an on-chain subscription that the Hedera Schedule Service extends by itself.
Live:https://retainer.edycu.dev Β· judging this? the 30-second read is at
https://retainer.edycu.dev/judge (JUDGE.md) β claim, four commands that
prove it against Hedera, the measured costs, and the limitations. Or try the gate yourself:
# a cold agent is charged
curl -i "https://retainer.edycu.dev/api/retainer/access?agent=0x0000000000000000000000000000000000000abc"# β 402, with an x402 challenge for hedera:testnet settled by Blocky402# read any agent's window without touching the payment path
curl -s "https://retainer.edycu.dev/api/retainer/status?agent=0xD14CA86A1483e9b2147a7B86fB74D437d3d2Cc66"An agent can pay for a thing. An agent cannot subscribe to a thing, because every renewal needs somebody awake to re-authorise it β a human clicking, or a cron job someone has to operate and keep alive. Retainer removes that person.
A walkthrough you can follow against the running server. The agent's address is passed as
?agent=0xβ¦; the only thing it ever signs is the payment in step 2.
1 β cold request, no access
GET /api/retainer/access?agent=0xAGENT
β 402 Payment Required
The route reads hasAccess(agent) on-chain, gets false, and builds x402 payment
requirements: scheme: exact, network: hedera:testnet, asset native HBAR, amount
RETAINER_PRICE_TINYBAR Γ RETAINER_PERIODS_PER_PURCHASE. The challenge body is emitted
verbatim in the PAYMENT-REQUIRED header so ordinary x402 clients can parse it.
2 β the agent pays once
The agent signs a Hedera TransferTransaction under the x402 exact scheme and retries with
the payment header. The resource server verifies and settles through the hosted Blocky402
facilitator (https://api.testnet.blocky402.com), which co-signs as fee payer and submits to
consensus. Nothing is served until funds are actually captured.
3 β the settled payment becomes on-chain state
This is the join between the two rails. The server takes the amount it just received and
calls RetainerAccess.subscribeFor(agent) with it. That opens the subscription, charges
period one, and arms the first scheduled renewal. The response carries both transactions:
If settlement succeeds but the on-chain forward fails, the request is still served and
subscription.opened is false with the error β the route never implies a subscription that
does not exist.
4 β the second request, inside the window
GET /api/retainer/access?agent=0xAGENT
β 200 { "paidThisRequest": false, β¦ }
No 402, no signature, no payment. The gate asked the chain one question and the answer was yes.
5 β a request after the window has expired
GET /api/retainer/access?agent=0xAGENT
β 200 { "paidThisRequest": false, β¦ }
Still 200. Nothing was paid, nobody was awake, no cron job ran. Between step 4 and step 5 the
Hedera Schedule Service called renew() on the contract, which charged the next period and
extended the window. One x402 payment buys RETAINER_PERIODS_PER_PURCHASE periods (default
3): the first is charged when the subscription opens, the rest are charged by unattended
renewals.
GET /api/retainer/status?agent=0xβ¦ is the read-only version of the same state β it touches
the chain and nothing else, so a UI can poll it without repeatedly opening payment challenges.
The page at / uses it to show the window counting down and then jumping back up on its own.
Two rails. The payment rail is off-chain HTTP that settles on Hedera; the renewal rail is
purely on-chain. They join in exactly one place: subscribeFor().

Same diagram as plain text
PAYMENT RAIL (once, at the start)
βββββββββββββββββββββββββββββββββ
Agent ββ1β GET /api/retainer/access ββββββΆ Resource server (Next.js)
ββ2β 402 + payment requirements β reads hasAccess(agent)
ββ3β sign Hedera "exact" transfer β
ββ4β retry with payment header ββββββββββΆβ
β 5 verify + settle
βΌ
Blocky402 facilitator (hosted)
β co-signs as fee payer,
β submits TransferTransaction
βΌ
Hedera testnet
ββ6β 200 + settlement reference ββββββββββ
ββ THE JOIN ββ
the server forwards what it received on-chain:
RetainerAccess.subscribeFor(agent) { value }
β
βΌ
RENEWAL RAIL (from here on, unattended)
βββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RetainerAccess.sol (Hedera testnet) β
β charges period 1, sets expiresAt β
β hasScheduleCapacity(expiresAt, gas) ββ ask first β
β scheduleCall(this, expiresAt, gas, renew(agent)) ββ arm β
βββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β at expiresAt, with no caller
βΌ
Hedera Schedule Service (HIP-1215, system contract 0x16b)
β executes CONTRACTCALL, scheduled=true
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β renew(agent): charge next period, extend window, β
β arm the following renewal ββββββββββββββββββββΌβββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β² β
βββββββββββββββββββββββββββββββββββββββββββββββββ
loops until money or gas reserve runs out,
or the subscriber cancels β always with an event
Later requests ask one on-chain question: hasAccess(agent) β 200, nothing paid.
| Layer | What |
|---|---|
| Resource server | Next.js 15 (App Router) + TypeScript β packages/nextjs |
| Payment | x402 exact scheme (@x402/core, @x402/hedera), settled by the hosted Blocky402 facilitator |
| Contract | Solidity 0.8.28, Hardhat β packages/hardhat |
| Self-renewal | Hedera Schedule Service (HIP-1215), system contract 0x16b |
| Chain | Hedera testnet β JSON-RPC via Hashio, artifacts re-verified against the public mirror node |
| Chain client | ethers v6 |
A subscription to nothing is not a product, so the gated resource is a real metered data feed, not a constant string.
It serves the live HBAR/USD rate the Hedera network itself uses, read from the mirror node
(/api/v1/network/exchangerate). That rate is not a third-party quote β it is the number the
network applies when converting its USD-denominated fee schedule into tinybar, which is what
makes Hedera fees predictable and sub-cent. An agent metering this feed is reading the same
number that priced its own transaction.
The charge is metered, not flat. A period does not buy unlimited use; it buys a countable
quantity of calls (callsPerPeriod). Every served call is counted on-chain by
meter(agent) before the response goes out, so the tally is auditable by the buyer rather than
asserted by the seller. Spend the allowance and the route answers 429 β the window is still
open, but what the period bought is used up.
And this is where the two halves meet: the unattended renewal refills the meter. The same
scheduled call that extends the access window resets callsUsed to zero. That is what makes a
self-renewing subscription worth having rather than a novelty.
{
"access": "granted",
"paidThisRequest": false,
"metering": {
"callsRemainingThisPeriod": 3,
"recordedOnChain": "0xβ¦"// one Hedera transaction per served call
},
"resource": {
"pair": "HBAR/USD",
"rate": 0.08258,
"raw": { "centEquivalent": 247738, "hbarEquivalent": 30000 },
"source": "https://testnet.mirrornode.hedera.com/api/v1/network/exchangerate"
}
}Metering each call on-chain costs one transaction per request, which is only reasonable because Hedera fees are sub-cent. On a chain with real gas this design would be indefensible, and that tradeoff is the honest reason it is written this way here rather than kept in a database.
RetainerAccess never mixes whose money is whose, and _solvent() asserts the contract's
balance still covers all three at the end of every call that moves money out of a pot β
subscribe/subscribeFor, renew, cancel and withdraw:
| Pot | Whose | Spent on |
|---|---|---|
_owed | the subscriber's | drawn down one period at a time; whatever is unspent is refunded on cancel() |
revenue | the seller's | withdrawable by beneficiary only |
gasReserve | the seller's | the network's fee for each scheduled execution |
Every amount is tinybar, and the contract converts nothing. Hedera has two denominations
and the boundary is not where an Ethereum instinct puts it: the JSON-RPC relay speaks
weibar (1 HBAR = 1e18), so the value you sign is 1e18-scaled, but inside the EVM
msg.value, address(this).balance and the value of an outbound call{value:} are all
tinybar (1 HBAR = 1e8). The relay converts at the edge. Adding the 1e10 conversion that
Ethereum experience asks for overpays every transfer by ten orders of magnitude, and it still
looks like a successful transaction. This was settled by measurement, not by reasoning:
packages/hardhat/contracts/test/UnitProbe.sol was deployed to testnet, sent 2 HBAR as 2e18 on
the wire, and reported msg.value == 200000000.
Three methods from HederaScheduleService (HIP-1215, system contract 0x16b), all
load-bearing β remove any one and the product breaks rather than degrades:
| Method | Where | What it is for |
|---|---|---|
scheduleCall | _armRenewal() | Arms the next renewal: asks the network to call renew(agent) on this contract at expiresAt. This is the product β without it there is only a cron job someone has to run. |
hasScheduleCapacity | _armRenewal() | Asked before arming. A second with no capacity becomes a clean Lapsed event instead of a revert or a subscription that silently stops. |
deleteSchedule | _releaseSchedule() | Releases the pending schedule on cancel() and returns its held gas to the reserve. Without it, subscribeβcancel churn is a free, repeatable drain of the seller's reserve. |
Two details that only show up on a real network:
- A scheduled call can see a block timestamp behind its own second. Observed on testnet
against contract
0.0.10406002: the schedule was armed forexpiresAt = 1788779924, the network executed it at consensus1788779924.038958161, andrenew()still reverted withCONTRACT_REVERT_EXECUTED. A strictblock.timestamp >= expiresAtgate therefore rejects the network's own call and self-renewal silently stops.renew()allowsRENEW_SLACK = 30seconds of earliness, andMIN_PERIOD_SECONDS = 61keeps that tolerance a strict minority of every period sorenew()cannot be looped by a third party at the seller's expense. - Lapsing is loud.
Lapsedis emitted before scheduling, because a scheduled call that cannot pay for itself fails withINSUFFICIENT_PAYER_BALANCEand emits nothing at all β the subscription would otherwise look alive forever while being dead.
There are three deployments on testnet, and they are not interchangeable:
| Contract | What it is | |
|---|---|---|
| Current | 0.0.10415845 / 0x433050c9bd203FBdd49FAB6b5E20eD3E1FB2a931 Β· HashScan | RetainerAccess.sol as it stands in this repo (7400cd7). It is what packages/nextjs/contracts/deployedContracts.ts points at, so it is the contract the resource server talks to. 9 unattended renewals, one full lapse cycle, and one scheduled execution that reverted (below). |
| Intermediate | 0.0.10414167 / 0xd3A218AD4c817B14Cc754e4c996A95435155a27B Β· HashScan | The units-corrected source before metering (9eb39e3). 7 unattended renewals, one cancel() that deleted a pending schedule, and the agent-script transcript at the end of docs/proof.md. |
| First | 0.0.10406083 / 0x8B42a662b0Bd5EecF09517840f63A61AAbEb952A Β· HashScan | The deployment that produced the cost table below. It predates the current constructor and ABI, so do not read it as a copy of the current source. 3 unattended renewals. |
19 renewals the network executed by itself, across the three β every one a CONTRACTCALL
with scheduled=true and SUCCESS, each with a Renewed event; the count and the commands
that reproduce it are in docs/proof.md. On the current deployment the loop
ran eight times in a row on 2026-09-08 with no submitter, from one ordinary renew() to a loud
Lapsed, and reproduced the cost split below at the current gas price: 154,036,168 tinybar for
a renewal that re-arms, 5,222,880 for the one that does not. All eight come back from a single
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'One scheduled execution on the current deployment reverted β 1788840415.078121802,
CONTRACT_REVERT_EXECUTED, with the contract's own Insolvent() guard, while the account held
239 million tinybar more than its three pots. It is the third of the honest limitations in
JUDGE.md and is worked through in docs/proof.md; it is not fixed here.
An x402 payment settled through Blocky402 (the one that opened the current deployment's
subscription, 3 β agent β seller, fee paid by the facilitator):
0.0.7162784@1788840225.936068496 β
and the first run's: 0.0.7162784@1788780154.225876092
The three renewals the first deployment's run produced, all CONTRACTCALL with
scheduled=true and status SUCCESS, read back from the mirror node. No transaction was sent
to trigger any of them:
| Consensus timestamp | Charged to the contract | What happened |
|---|---|---|
1788780226.016366208 | 154,896,000 tinybar = 1.54896 β | renewed and re-armed the next |
1788780286.019735208 | 154,896,000 tinybar = 1.54896 β | renewed and re-armed the next |
1788780346.345418842 | 5,067,825 tinybar = 0.0507 β | charged the last period, then found nothing left for a fifth: emitted Lapsed("balance will not cover the next period") and did not re-arm |
Gas used on testnet: subscribe()1,582,554 (limit 2,000,000), deploy 968,564.
The third row above is the whole cost story, and it is the most interesting thing this build measured. A renewal that re-arms the next one costs 1.54896 HBAR. A renewal that does not re-arm costs 0.0507 HBAR. That is a ~30Γ gap between two executions of the same function, and it means:
Re-arming the next renewal β the
scheduleCallinto0x16bβ is roughly 97% of what a renewal costs. The renewal's own bookkeeping is the cheap 0.05 HBAR part.
The local Hardhat gas report, which mocks the scheduler and therefore excludes the
system-contract call, puts renew() at 48,247β77,085 gas and subscribe() at
137,552β205,722. The ~1.4M difference against testnet is the real scheduleCall. If you
only ever measure locally, you will not see the cost of this product at all.
It gets worse before it gets better: Hedera refunds at most 20% of an unused gas limit, so
RENEWAL_GAS_LIMIT = 2_500_000 is charged at roughly 2,000,000 gas whether or not it is used.
The consequence, stated plainly: at the default price of 1 HBAR per period, Retainer loses
money on every renewal, because each renewal burns ~1.55 HBAR of the seller's gas reserve
to collect 1 HBAR of revenue. RENEWAL_COST_ESTIMATE (2 HBAR) is held out of the reserve per
armed renewal for exactly this reason, and renewalsRemaining() reports how many the reserve
can still afford.
This is the real constraint of on-chain self-renewal, and it is not solved here. Two things move it:
- Right-size
RENEWAL_GAS_LIMITtoward the ~1.5M actually used. Recovers roughly a third. - Price a period above the renewal cost. This is what actually makes it solvent, and it
is a product decision, not a code one: break-even is the ~1.55 HBAR a re-arming renewal
actually costs, and the reserve drains at the 2 HBAR
RENEWAL_COST_ESTIMATEthe contract holds back per armed renewal. Below that, the seller is paying for its own users.
The full measurement β how Hedera charges a scheduled call, what the 1.549-vs-0.051 split
proves, and each option sized honestly β is in docs/gas-economics.md.
Every transaction, schedule and event behind the table above, with curl commands that
re-verify all of it against the public mirror node, is in docs/proof.md.
A subscription therefore ends loudly rather than silently. Renewal stops with a Lapsed event
carrying its own reason string β "balance will not cover the next period",
"gas reserve will not cover the next renewal", "no schedule capacity at that second",
"network refused the schedule", or the defensive "insufficient subscriber balance" β and
the first four are asserted by name in the test suite. A subscriber who cancels is a separate
event, Cancelled: an ending they chose, not one that surprised them.
No Docker, no object storage, no self-hosted facilitator. Settlement uses the hosted Blocky402 testnet facilitator, which supplies its own fee payer.
Node.js β₯ 20.18.3 (Node 20 LTS), Yarn 3 via Corepack
(corepack enable && corepack prepare yarn@stable --activate), and a funded ECDSA Hedera
testnet account from the Hedera Portal faucet. ECDSA is required
β x402 on Hedera will not work with an ED25519 key.
git clone https://github.com/edycutjong/retainer.git
cd retainer
yarn installcp packages/hardhat/.env.example packages/hardhat/.env
yarn hardhat:account:generate # or: yarn hardhat:account:import
yarn hardhat:compile
yarn hardhat:testFund the printed deployer account with testnet HBAR before deploying.
RETAINER_PRICE_TINYBAR=100000000 \
RETAINER_PERIOD_SECONDS=3600 \
yarn hardhat:deploy --network hederaTestnetAfter deploying, the script seeds the gas reserve with 8 HBAR (RETAINER_RESERVE_HBAR) in a
separate fundGasReserve() call β not as constructor value, because Hedera credits a
contract-create's initial balance outside the EVM frame, where a payable constructor cannot book
it. A self-renewing contract has to hold gas for its own future; at the 2 HBAR
RENEWAL_COST_ESTIMATE the contract holds back per armed renewal, 8 HBAR arms four of them.
The script writes the address and native 0.0.x contract id into
packages/nextjs/contracts/deployedContracts.ts, which the resource server reads
automatically.
cp packages/nextjs/.env.example packages/nextjs/.envSet these (the rest of the file has sensible defaults):
| Variable | What it is |
|---|---|
HEDERA_RPC_URL | JSON-RPC endpoint. Defaults to https://testnet.hashio.io/api. |
FACILITATOR_URL | https://api.testnet.blocky402.com. Already the code default. |
RETAINER_PAY_TO | The seller's Hedera account id (0.0.x) that x402 payments go to. |
RETAINER_PRICE_TINYBAR | Price of one period. Default 100000000 (1 HBAR). Must match the contract's terms. |
RETAINER_PERIODS_PER_PURCHASE | How many periods one payment buys. Default 3. |
RETAINER_SERVER_KEY | ECDSA key of the seller account that forwards settled payments on-chain. Without it the 402 still settles but no subscription opens. |
RETAINER_ACCESS_ADDRESS | Optional override; normally resolved from deployedContracts.ts. |
Then:
yarn next:devhttp://localhost:3000β the landing page and live view. The instrument at the top replays the recorded testnet run (four real transactions fromdocs/proof.md, labelled as recorded, 10Γ time) or watches any agent live; paste an address and the window counts down and then extends itself. Every live number is read from chain state via/api/retainer/status; nothing is simulated.http://localhost:3000/api/retainer/access?agent=0xβ¦β the gate.http://localhost:3000/api/retainer/status?agent=0xβ¦β read-only state, safe to poll.http://localhost:3000/judgeβ the judge-facing summary. Static, no auth, no chain call, so it renders even when the network does not.
packages/nextjs/scripts/retainer-agent.ts is the demo as an agent experiences it: cold
request β 402 β pay via 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. It reads BUYER_PRIVATE_KEY, BUYER_ACCOUNT_ID and
RETAINER_ACCESS_ADDRESS from ~/.config/retainer/hedera.env (credentials live outside the
repo):
cd packages/nextjs
BASE_URL=http://localhost:3000 yarn tsx scripts/retainer-agent.tsIf RETAINER_SERVER_KEY is not configured the server cannot forward the payment, so the
script opens the subscription itself with the agent's own key and says so β the rest of the
run is unchanged. It waits the seller's own periodSeconds (plus 45s of slack) unless
PERIOD_SECONDS overrides it.
packages/hardhat/scripts/proveRenewal.ts is the narrower proof that produced the testnet
measurements above: it reads the seller's terms off the contract, subscribes for three
periods, then sends nothing and re-reads subscriptionOf to show the window extended on its
own. It then cancels and compares the refund against the wallet balance, which is the
regression guard for the tinybar/weibar bug.
yarn hardhat:test46 passing in packages/hardhat/test/RetainerAccess.test.ts, grouped by the thing each
group protects: tinybar/weibar unit handling, the seller β not the subscriber β setting the
price, x402 settlement crediting the on-chain subscription, the renew() time gate that
closes the griefing vector, what the contract actually asks the scheduler to do, separation of
the three money pots, metering, lapsing loudly in every failure case, and the access gate
itself. MockScheduleService.sol stands in for the 0x16b system contract locally β which is
exactly why local gas numbers understate the real cost, as measured above.
Test names describe the defect they pin rather than the function they call, so the list reads
as a changelog of the bugs this build actually had β "accepts a renewal that fires slightly
EARLY, as the real scheduler does" is the testnet observation above, turned into a guard.
Six of them are permission-boundary tests: the beneficiary cannot reach subscriber money or
the gas reserve, a stranger cannot loop renew(), and subscribeFor cannot spend an agent's
balance without funding a period. Each claim is listed next to its test in
.github/SECURITY.md.
yarn next:test # 10 unit tests, the resource server's arithmeticThe sharpest edge in this project β Hedera's weibar/tinybar boundary, whose failure mode is a
transfer wrong by ten orders of magnitude that still returns a successful receipt β cannot be
guarded by examples, because examples are exactly what a 1e10 error survives. So the
conversion is verified across a range instead: 202,059 distinct amounts against three
invariants each (606,177 assertions) β every value from 0 to 100,000, every value across the
1 HBAR seam, every decade edge up to 1e18 including the 1e10 factor itself, and 100,000
randomised uint64 amounts. The oracle is the UnitProbe measurement from testnet, not a
restatement of the implementation. See docs/hedera-units.md.
yarn e2e # 56 Playwright checks, no credentials requiredThe E2E suite asserts the one thing a paywall must never do. With no seller account
configured, /api/retainer/access is sent a cold agent and the answer must not be 200 β
402, 500, 502 and 503 are all correct refusals; a served feed is not. That the suite needs no
credentials is a property of the tests, not of the product: Retainer has no offline or mock
mode, and the paid path is proven against the live network in
docs/proof.md. It found two real layout bugs on /judge the first time it
ran, both at 375px, both invisible from a desktop. e2e/landing.spec.ts pins what a judge relies
on at /: one claim, the instrument in the first viewport, the recorded run labelled as recorded,
an explicit switch to the live chain, no autoplay under reduced motion, no sideways scroll on a phone.
| Layer | Tool | Where |
|---|---|---|
| Contract tests | Hardhat + Mocha, 46 passing | .github/workflows/lint.yaml |
| Unit tests | Vitest + fast-check, 10 passing, 202,059 amounts | .github/workflows/lint.yaml |
| E2E | Playwright, 56 checks, desktop + mobile | .github/workflows/e2e.yaml |
| Types + lint | tsc --noEmit and ESLint, both workspaces | .github/workflows/lint.yaml |
| SAST | CodeQL β TypeScript and the Actions workflows | .github/workflows/codeql.yaml |
| Secrets | gitleaks over the full history, fetch-depth: 0 | .github/workflows/gitleaks.yaml |
| Dependencies | Dependabot, grouped and monthly, majors ignored | .github/dependabot.yml |
| Performance | Lighthouse CI + a bundle-size tripwire | .github/workflows/e2e.yaml |
| Deploy gate | verify β Vercel β live 402 smoke test | .github/workflows/deploy.yaml |
The deploy workflow is the one worth a second look: it refuses to promote a build whose live
gate has stopped answering 402 for hedera:testnet. A green deploy badge here means the
product still works, not that Vercel accepted an upload.
CodeQL deliberately does not claim to cover the Solidity β there is no CodeQL extractor for it, and a green checkmark that means nothing is worse than an absent one. The contract's security properties are asserted by named tests instead.
yarn ci runs the compile, both test suites, both lints and both type checks in one command.
packages/hardhat/
contracts/RetainerAccess.sol the subscription + self-renewal contract
contracts/test/MockScheduleService.sol local stand-in for system contract 0x16b
contracts/test/UnitProbe.sol the tinybar/weibar measurement, run on testnet
deploy/01_deploy_retainer_access.ts deploys, then funds the gas reserve
scripts/proveRenewal.ts subscribe, send nothing, watch it renew
test/RetainerAccess.test.ts 46 tests
packages/nextjs/
app/api/retainer/access/route.ts the x402 gate: 402, settle, subscribeFor
app/api/retainer/status/route.ts read-only chain state, safe to poll
app/page.tsx the landing page and live view
components/landing/ the instrument: recorded run (from docs/proof.md) + live chain
services/retainer/server.ts contract reads + forwarding settled payments
services/x402/server.ts x402 resource server, Blocky402 facilitator
app/judge/page.tsx /judge β the 30-second read for one reader
test/units.property.test.ts the unit boundary, 202,059 amounts
scripts/retainer-agent.ts the whole flow as an agent runs it
e2e/ Playwright: the gate must fail closed
JUDGE.md what /judge says, for whoever arrives from GitHub
specs/ architecture and provenance
prompts/ the prompts that directed the build
docs/proof.md every on-chain artifact, and how to re-verify it
docs/gas-economics.md what an unattended renewal actually costs
docs/hedera-units.md the weibar/tinybar trap, and the probe that settled it
.github/SECURITY.md each security claim, next to the test that pins it
MIT β see LICENCE. The file retains the original copyright line from the
scaffold-hbar template it was inherited from.
This repository was built from hedera-dev/scaffold-hbar,
branch templates/x402-pay-per-use β the starter template Hedera's own bounty page lists as
official. Saying so plainly is the point: it is permitted, and hiding it would read far worse.
The template's own product β a MinIO-backed pay-per-download file marketplace with a
FileRegistry contract, a block explorer, docker-compose, and a self-hosted facilitator β
has been removed. What remains from it is the Hedera wallet/RPC plumbing, the Hardhat
setup, and the x402 client/server wiring. RetainerAccess.sol, both API routes, the retainer
service layer, the live view, the agent script, and the tests are this project's own.
Full file-by-file accounting, including a correction to an earlier overstatement, is in
specs/provenance.md. AI attribution per file is in
AI-USAGE.md; the prompts that directed the build are in
prompts/.
{ "access": "granted", "paidThisRequest": true, "payment": { "transaction": "0.0.β¦@β¦", "payer": "0.0.β¦" }, "subscription": { "opened": true, "transaction": "0xβ¦", "periodsPurchased": 3 } }