Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
566 changes: 165 additions & 401 deletions AGENTS.md

Large diffs are not rendered by default.

8 changes: 2 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
Read and follow the instructions in `AGENTS.md`.

`AGENTS.md` is the single agent-instruction file for this repo — ICP skills
setup, project conventions, the issue tracker, triage labels, and the
verification gate all live there. This file exists only to point Claude Code at
it, so that agent instructions never drift between two copies.
Read and follow `AGENTS.md`. It is the single agent-instruction file for this repo;
this file only points at it, so the two never drift.
105 changes: 35 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,31 @@
# CyclePay — Fully On-Chain Cycles Gateway

**Buy cycles with a credit card.** No ICP, no wallet, no exchange account — which is the
whole point: it gets a developer from "I have a card" to "my canister has cycles" without
first solving crypto onboarding.
**Buy cycles with a credit card.** No ICP, no wallet, no exchange account: it gets a
developer from "I have a card" to "my canister has cycles" without first solving crypto
onboarding.

It runs entirely on the Internet Computer — one Motoko backend canister and one
It runs entirely on the Internet Computer, one Motoko backend canister and one
certified-assets frontend canister, **no server**. It sells cycles from a reserve it
already holds, prices them from two on-chain rates with **no outbound HTTPS in the pricing
path**, and shows the buyer the cycle quantity before they commit.

Everything money-touching **fails closed**: a freshly deployed gateway accepts no orders
and delivers nothing until each lever is consciously set.
already holds, prices them from two on-chain rates with no outbound HTTPS in the pricing
path, and shows the buyer the cycle quantity before they commit. Everything
money-touching **fails closed**: a freshly deployed gateway accepts no orders and
delivers nothing until each lever is consciously set.

## It is running

**<https://cyclepay.raymondk.co>** — or <https://shy4u-4qaaa-aaaay-aadhq-cai.icp.net>,
which is the same app on the canister's own gateway origin. Internet Identity derives
principals from the **frontend canister id**, so both addresses give you the same
account and the same cycles.

⚠️ **Simulation mode, and you cannot buy on it.** Cards are charged in Stripe's
sandbox, and cycles are divided by `pricing_status().config.divisor` — so a purchase
quotes *and* delivers that fraction of what a live gateway would for the same charge.
Read the divisor rather than trusting a number written here.
**<https://cyclepay.raymondk.co>**, or <https://shy4u-4qaaa-aaaay-aadhq-cai.icp.net>,
the same app on the canister's own gateway origin. Internet Identity derives principals
from the **frontend canister id**, so both addresses give the same account.

Purchases also require the buyer's principal to be **allow-listed by a controller**:
without that, free sandbox payments against a funded reserve would be a faucet, so an
unlisted principal is refused with `buyerNotAllowed`.
⚠️ **Simulation mode, and you cannot buy on it.** Cards are charged in Stripe's sandbox,
and cycles are divided by `pricing_status().config.divisor`, so a purchase quotes *and*
delivers that fraction of what a live gateway would. Purchases also require the buyer's
principal to be allow-listed by a controller: without that, free sandbox payments
against a funded reserve would be a faucet, and an unlisted principal is refused with
`buyerNotAllowed`.

What anyone can do on it, with no identity at all: browse, read a **live quote** for any
amount, and check every operational number the gateway publishes.
Anyone can browse, read a live quote for any amount, and check every operational number
the gateway publishes, with no identity:

```bash
icp canister call backend quote_previews '(vec { 1_000 : nat })' -e ic # $10, with both rate inputs
Expand All @@ -42,21 +38,18 @@ icp canister call backend card_tiers '()' -e ic
Canister ids are in `.icp/data/mappings/ic.ids.json`; the backend is
`saz2a-riaaa-aaaay-aadha-cai`.

This repository began as a fork of [`raymondk/cyclepay`](https://github.com/raymondk/cyclepay),
which explored the design with an agent loop; it is the source of truth now, and the
architecture it ships is not the one the fork point described.

## How it works, and how to run it

**[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) has the diagram** — the money path end to end, where the trust
boundaries sit, and which of the two cycle pots a delivery spends from. Start there.
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) has the diagram: the money path end to
end, where the trust boundaries sit, and which of the two cycle pots a delivery spends
from. Start there.

| you want to | go to |
|---|---|
| understand the system | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), then [`docs/DESIGN.md`](docs/DESIGN.md) for *why* |
| run it locally | [`docs/OPERATE.md`, Mode 1](docs/OPERATE.md#mode-1--local) |
| deploy it | [Mode 2 — mainnet simulation](docs/OPERATE.md#mode-2--mainnet-simulation) or [Mode 3 — production](docs/OPERATE.md#mode-3--mainnet-production) |
| operate one that is misbehaving | [`RUNBOOK.md`](RUNBOOK.md#enter-here-what-you-are-looking-at) — entered by symptom |
| operate one that is misbehaving | [`RUNBOOK.md`](RUNBOOK.md#enter-here-what-you-are-looking-at), entered by symptom |
| check the claims yourself | [`docs/VERIFY.md`](docs/VERIFY.md#what-anyone-can-check-right-now) |

```sh
Expand All @@ -65,21 +58,7 @@ icp network start -d && icp deploy && scripts/local-dev-seed.sh
```

⚠️ **The submodule and the seed are both load-bearing**, and neither failure looks like
its cause — [`docs/OPERATE.md`, Mode 1](docs/OPERATE.md#mode-1--local) explains both
before the first command.

## Verify it yourself

`docs/VERIFY.md` is the list: [what a stranger can
check](docs/VERIFY.md#what-anyone-can-check-right-now) with no identity, [what only a
buyer can](docs/VERIFY.md#what-a-buyer-can-check-that-a-visitor-cannot), and [the
limits](docs/VERIFY.md#the-limits-in-the-same-breath) stated in the same breath.

The running backend is a published release: `scripts/release.sh` built it in a pinned
container, installed that artifact, and gated on the canister reporting the same hash. So
the module hash on chain has a counterpart anyone can reproduce — `docs/VERIFY.md` has
[the commands](docs/VERIFY.md#is-the-backend-module-built-from-this-repo--compare-it-to-a-release)
and [the limits that remain](docs/VERIFY.md#the-limits-in-the-same-breath).
its cause. [`docs/OPERATE.md`, Mode 1](docs/OPERATE.md#mode-1--local) explains both.

## Documents

Expand All @@ -88,37 +67,23 @@ For a **reader or verifier**:
| | |
|---|---|
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | The diagram: canisters, money path, trust boundaries, the two cycle pots |
| [`docs/DESIGN.md`](docs/DESIGN.md) | The decision record — *why* it is built this way. What the `§N` comments point at. Gate-enforced |
| [`docs/STRIPE.md`](docs/STRIPE.md) | The Card rail end to end, written from the code: ingress, signature verification, attribution, dedup, pricing, the order lifecycle, refunds |
| [`docs/DESIGN.md`](docs/DESIGN.md) | The decision record, *why* it is built this way. What the `§N` comments point at; gate-enforced |
| [`docs/STRIPE.md`](docs/STRIPE.md) | The Card rail end to end, written from the code |
| [`docs/TEST-COVERAGE.md`](docs/TEST-COVERAGE.md) | What is tested, how, and what is not |
| [`docs/VERIFY.md`](docs/VERIFY.md) | What anyone can check about the live deployment, what they cannot, and the measured state of the module-hash chain |
| [`docs/VERIFY.md`](docs/VERIFY.md) | What anyone can check about the live deployment, and what they cannot |
| [`docs/BUYER-COST-MODEL.md`](docs/BUYER-COST-MODEL.md) | What a purchase buys, and why the floor is $10 |

For an **operator**:

| | |
|---|---|
| [`docs/OPERATE.md`](docs/OPERATE.md) | Setup, one procedure per mode: local, mainnet simulation, mainnet production |
| [`RUNBOOK.md`](RUNBOOK.md) | Day-2 operations, entered by symptom: secret rotation, rate diagnosis, reserve sizing, obligation triage, monitoring |
| [`RUNBOOK.md`](RUNBOOK.md) | Day-2 operations, entered by symptom |
| [`RELEASE.md`](RELEASE.md) | Cutting a release: build, publish hashes, install, gate |
| [`CHANGELOG.md`](CHANGELOG.md) | What changed per release |
| [`docs/SANDBOX-TESTPLAN.md`](docs/SANDBOX-TESTPLAN.md) | The manual Stripe-sandbox pass required before go-live, and what a green run does not prove |

For an **agent changing the code**: [`AGENTS.md`](AGENTS.md) (conventions, skills, the verification
gate), and [`docs/DESIGN.md`](docs/DESIGN.md) above — it is the primary surface for that audience, along
with the invariant comments in `src/backend`. `docs/agents/` holds the loop's own
conventions: the triage labels, the issue-tracker rules and `deleted-vocabulary.md`.

Also: [`docs/DEMO-PLAYBOOK.md`](docs/DEMO-PLAYBOOK.md), the running order for demoing this to a technical
audience.

## Release

[`RELEASE.md`, Cutting a release](RELEASE.md#cutting-a-release) is the procedure: build in
a digest-pinned container, publish the module hashes, install **that artifact**, and gate
on the canister reporting the hash that was built. [`CHANGELOG.md`](CHANGELOG.md) records what each release changed, and the
release script refuses a version that has no entry there.
| [`CHANGELOG.md`](CHANGELOG.md) | What changed per release; the release script refuses a version with no entry |
| [`docs/SANDBOX-TESTPLAN.md`](docs/SANDBOX-TESTPLAN.md) | The manual Stripe-sandbox pass required before go-live |
| [`docs/DEMO-PLAYBOOK.md`](docs/DEMO-PLAYBOOK.md) | The running order for demoing this to a technical audience |

⚠️ **`backend.wasm` depends on the build architecture**, so each release states the one
it was built on and a verifier has to match it. `frontend.wasm` is the pinned recipe's
module and proves nothing about the page — [`docs/VERIFY.md`](docs/VERIFY.md) has the
check that does.
For an **agent changing the code**: [`AGENTS.md`](AGENTS.md) (conventions, skills, the
verification gate) and `docs/DESIGN.md`. `docs/agents/` holds the loop's own
conventions: the issue-tracker rules and `deleted-vocabulary.md`.
Loading
Loading