diff --git a/.kiro/specs/notify-chain-improvements/.config.kiro b/.kiro/specs/notify-chain-improvements/.config.kiro
new file mode 100644
index 0000000..8d19582
--- /dev/null
+++ b/.kiro/specs/notify-chain-improvements/.config.kiro
@@ -0,0 +1 @@
+{"specId": "16f73cd2-f721-43da-8f45-21b0feb74e4c", "workflowType": "design-first", "specType": "feature"}
diff --git a/.kiro/specs/notify-chain-improvements/design.md b/.kiro/specs/notify-chain-improvements/design.md
new file mode 100644
index 0000000..57633ed
--- /dev/null
+++ b/.kiro/specs/notify-chain-improvements/design.md
@@ -0,0 +1,431 @@
+# Design Document: Notify-Chain Improvements
+
+## Overview
+
+This document covers three independent improvements to the Notify-Chain repository:
+
+1. **Task 1 — Secure Credential Management**: Harden the repository against accidental secret leakage by closing a `.gitignore` gap in the dashboard, completing missing entries in `listener/.env.example`, and adding a unified Configuration reference section to the root `README.md`.
+
+2. **Task 2 — Gas Consumption Tracking**: Introduce a reproducible gas-measurement test module for the AutoShare Soroban contract, persist the results as a JSON snapshot file, document human-readable cost figures in `GAS_USAGE.md`, and add a CI gate that fails when any function regresses beyond 10 % of its baseline.
+
+3. **Task 3 — Subscription Edge-Case Tests**: Expand the Rust test suite for the `topup_subscription` and `reduce_usage` functions to cover the eight missing edge cases identified during review, and register the new test module in `lib.rs`.
+
+Each task is self-contained and can be committed to its own branch independently.
+
+---
+
+## Architecture
+
+```mermaid
+graph TD
+ subgraph Task1["Task 1 · Credential Management"]
+ A1["dashboard/.gitignore
add .env"]
+ A2["listener/.env.example
add missing vars"]
+ A3["README.md
add Config section"]
+ end
+
+ subgraph Task2["Task 2 · Gas Tracking"]
+ B1["gas_snapshot_test.rs
measure budget per fn"]
+ B2["contract/gas-snapshots/autoshare.json
baseline values"]
+ B3["GAS_USAGE.md
human docs"]
+ B4[".github/workflows/ci.yml
regression gate step"]
+ B1 -->|writes| B2
+ B2 -->|read by| B4
+ B1 -->|documents| B3
+ end
+
+ subgraph Task3["Task 3 · Subscription Edge Cases"]
+ C1["subscription_edge_cases_test.rs
8 new test functions"]
+ C2["lib.rs
register module"]
+ C1 -->|registered via| C2
+ end
+```
+
+---
+
+## Components and Interfaces
+
+See per-task subsections below.
+
+## Data Models
+
+See per-task subsections below.
+
+## Error Handling
+
+See per-task subsections below.
+
+## Testing Strategy
+
+See per-task subsections below.
+
+---
+
+## Task 1 — Secure Credential Management
+
+### Components and Interfaces
+
+#### Component: `dashboard/.gitignore`
+
+**Purpose**: Prevent the dashboard's `.env` file from being committed.
+
+**Change**: Append `.env` to the existing ignore list.
+
+**Current state**:
+```
+node_modules
+dist
+.DS_Store
+```
+
+**Target state** (additions only):
+```
+.env
+.env.local
+.env.*.local
+```
+
+#### Component: `listener/.env.example`
+
+**Purpose**: Document every environment variable the listener reads so operators can configure a new deployment without reading source code.
+
+**Missing variables** (identified by diffing `config.ts` against `.env.example`):
+
+| Variable | Type | Default | Description |
+|---|---|---|---|
+| `DISCORD_WEBHOOK_ID` | string | — | The numeric ID portion of the Discord webhook URL; required when `DISCORD_WEBHOOK_URL` is set |
+| `RATE_LIMIT_ENABLED` | boolean | `true` | Set to `false` to disable the API rate limiter |
+| `RATE_LIMIT_WINDOW_MS` | integer | `60000` | Rolling window length for rate-limit counting (ms) |
+| `RATE_LIMIT_MAX_REQUESTS` | integer | `60` | Max requests per client within the window |
+| `RATE_LIMIT_CLIENT_OVERRIDES` | JSON object | `{}` | Per-client rate-limit overrides, keyed by client ID |
+
+These are already parsed by `loadConfig()` in `listener/src/config.ts`; the example file simply lacks corresponding documentation entries.
+
+#### Component: `README.md` (root)
+
+**Purpose**: Provide a single, authoritative reference for all environment variables so contributors do not need to read source files.
+
+**New section**: `## Configuration / Environment Variables`
+
+The section will contain two sub-sections:
+
+- **Listener (`listener/.env`)** — all variables from `listener/.env.example` with type, default, and description
+- **Dashboard (`dashboard/.env`)** — all variables from `dashboard/.env.example` with type, default, and description
+
+### Data Models
+
+No new data structures are introduced. The change is purely to non-code files.
+
+### Error Handling
+
+Not applicable — these are documentation and configuration changes.
+
+### Testing Strategy
+
+**Unit Testing Approach**: Not applicable for `.gitignore` and `.env.example` edits.
+
+**Property-Based Testing Approach**: Not applicable.
+
+**Integration Testing Approach**: Manual smoke-test by verifying `git status` does not surface a created `.env` file in the dashboard directory after the `.gitignore` change is applied.
+
+---
+
+## Task 2 — Gas Consumption Tracking
+
+### Architecture
+
+```mermaid
+sequenceDiagram
+ participant CI as CI Job (cargo test)
+ participant GasTest as gas_snapshot_test.rs
+ participant Budget as env.budget()
+ participant Snapshot as autoshare.json
+ participant Gate as CI regression step
+
+ CI->>GasTest: cargo test --all-features
+ GasTest->>Budget: reset_unlimited() before call
+ GasTest->>Budget: measure cpu_insns + mem_bytes after call
+ GasTest->>Snapshot: write baseline (first run)
+ CI->>Gate: read autoshare.json + compare
+ Gate-->>CI: fail if delta > 10%
+```
+
+### Components and Interfaces
+
+#### Component: `gas_snapshot_test.rs`
+
+**Purpose**: Measure and assert Soroban budget consumption for each critical function under representative inputs.
+
+**Location**: `contract/contracts/hello-world/src/tests/gas_snapshot_test.rs`
+
+**Key functions measured**:
+
+```rust
+// Pseudocode — actual Soroban testutils API
+fn measure_budget(env: &Env, call: impl FnOnce()) -> BudgetSnapshot {
+ env.budget().reset_unlimited();
+ call();
+ BudgetSnapshot {
+ cpu_insns: env.budget().cpu_insns_consumed(),
+ mem_bytes: env.budget().mem_bytes_consumed(),
+ }
+}
+```
+
+Each test function follows this pattern:
+
+```rust
+#[test]
+fn snapshot_create() {
+ let test_env = setup_test_env();
+ let token = test_env.mock_tokens.get(0).unwrap();
+ let creator = test_env.users.get(0).unwrap();
+ mint_tokens(&test_env.env, &token, &creator, 10_000_000);
+
+ test_env.env.budget().reset_unlimited();
+ let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
+ client.create(&id, &name, &creator, &10u32, &token);
+
+ let cpu = test_env.env.budget().cpu_insns_consumed();
+ let mem = test_env.env.budget().mem_bytes_consumed();
+
+ // Assert values are non-zero and within plausible range
+ assert!(cpu > 0);
+ assert!(mem > 0);
+ // Print for snapshot capture: println!("create: cpu={cpu}, mem={mem}");
+}
+```
+
+Functions covered:
+- `create` — baseline: 1 group, 10 usages, mock token, no members yet
+- `topup_subscription` — baseline: existing group, 10 additional usages, same payer
+- `update_members` — baseline: group with 3 members replacing existing empty list
+- `withdraw` — baseline: admin withdraws 100 tokens to recipient
+
+#### Component: `contract/gas-snapshots/autoshare.json`
+
+**Purpose**: Machine-readable baseline that the CI regression gate reads.
+
+**Schema**:
+
+```json
+{
+ "version": 1,
+ "functions": {
+ "create": {
+ "cpu_insns": 0,
+ "mem_bytes": 0,
+ "description": "Create a new AutoShare group (10 usages, no members)"
+ },
+ "topup_subscription": {
+ "cpu_insns": 0,
+ "mem_bytes": 0,
+ "description": "Top up existing group with 10 additional usages"
+ },
+ "update_members": {
+ "cpu_insns": 0,
+ "mem_bytes": 0,
+ "description": "Set 3 members with equal percentage split"
+ },
+ "withdraw": {
+ "cpu_insns": 0,
+ "mem_bytes": 0,
+ "description": "Admin withdraws 100 tokens"
+ }
+ }
+}
+```
+
+Values are initialised to `0` in the committed skeleton; the first successful CI run (or a local `cargo test -- --nocapture` run) populates them with real measurements that are then committed.
+
+#### Component: `GAS_USAGE.md`
+
+**Purpose**: Human-readable explanation of what each function costs and how to interpret or update snapshots.
+
+**Sections**:
+- Overview of Soroban resource metering
+- Table of function baselines (populated after first measurement run)
+- How to update the snapshot (`cargo test gas_snapshot`)
+- CI regression policy (10 % tolerance)
+
+#### Component: CI regression gate
+
+**Purpose**: Detect performance regressions before merge.
+
+**Mechanism**: After the existing `cargo test` step, add a new step that:
+1. Re-runs only the gas snapshot tests with `-- --nocapture` to capture printed measurements
+2. Parses the output lines (format: `FUNCTION: cpu=NNN mem=NNN`)
+3. Compares against `autoshare.json` baselines
+4. Exits non-zero if any metric exceeds `baseline * 1.10`
+
+The step uses a small inline shell script (no external action required) that reads the JSON with `jq` and performs integer arithmetic.
+
+```yaml
+- name: Gas regression check
+ working-directory: contract
+ run: |
+ output=$(cargo test --all-features gas_snapshot -- --nocapture 2>&1)
+ echo "$output"
+ baseline=$(cat contracts/hello-world/gas-snapshots/autoshare.json)
+ # parse and compare using jq + awk
+ # exit 1 if any function exceeds 110% of baseline
+```
+
+### Data Models
+
+```
+BudgetSnapshot {
+ cpu_insns: u64,
+ mem_bytes: u64,
+}
+```
+
+This is an ephemeral struct used only within the test module — it is never stored on-chain.
+
+### Error Handling
+
+- If the snapshot JSON is malformed, `jq` will exit non-zero and the CI step will fail with a clear parse error.
+- If the snapshot file is missing, the gate step skips comparison and emits a warning (does not fail — first-run scenario).
+
+### Testing Strategy
+
+**Unit Testing Approach**: Each snapshot test asserts that `cpu_insns > 0` and `mem_bytes > 0`, acting as a sanity check that measurement is working.
+
+**Property-Based Testing Approach**: Not applicable — gas measurement is deterministic for a given input, so PBT adds no value here.
+
+**Integration Testing Approach**: The CI gate is itself an integration test between the test output and the baseline JSON.
+
+---
+
+## Task 3 — Subscription Edge-Case Tests
+
+### Architecture
+
+All new tests live in a single new file and are registered in the existing `lib.rs` `tests` module block. No production code changes are required — the edge cases exercise existing contract logic.
+
+```mermaid
+graph LR
+ LIB["lib.rs\n#[path] mod subscription_edge_cases_test"] --> ECT["subscription_edge_cases_test.rs"]
+ ECT -->|uses| TU["test_utils.rs\nsetup_test_env, create_test_group, mint_tokens"]
+ ECT -->|calls| CLIENT["AutoShareContractClient\ntopup_subscription, reduce_usage, create, get"]
+```
+
+### Components and Interfaces
+
+#### Component: `subscription_edge_cases_test.rs`
+
+**Purpose**: Cover the eight missing edge cases for subscription lifecycle.
+
+**Location**: `contract/contracts/hello-world/src/tests/subscription_edge_cases_test.rs`
+
+**Test inventory**:
+
+| # | Test name | Expected behaviour | Error variant |
+|---|---|---|---|
+| 1 | `test_duplicate_create_error_type` | `create` with same ID panics with `AlreadyExists` | `Error::AlreadyExists` |
+| 2 | `test_topup_nonexistent_group` | `topup_subscription` on unknown ID panics | `Error::NotFound` |
+| 3 | `test_topup_inactive_group_succeeds` | `topup_subscription` on deactivated group **succeeds**; `usage_count` and `total_usages_paid` both increase | — |
+| 4 | `test_multiple_sequential_topups` | Three consecutive topups accumulate correctly in `usage_count` and `total_usages_paid` | — |
+| 5 | `test_reduce_to_zero_then_reduce_again` | `reduce_usage` N times to reach zero, next call panics | `Error::NoUsagesRemaining` |
+| 6 | `test_large_usage_count` | `topup_subscription` with 10 000 usages; verify arithmetic exact | — |
+| 7 | `test_topup_different_payer` | Third-party address (not the creator) successfully tops up | — |
+| 8 | `test_topup_insufficient_balance` | Payer has 0 tokens; call panics with token transfer failure | token panic |
+
+**Key design decisions**:
+
+- **Test 3 (inactive group topup)**: The `topup_subscription` logic in `autoshare_logic.rs` does **not** check `is_active` — it only checks that the group exists. Therefore, topping up a deactivated group succeeds. The test must verify this is intentional by asserting the state after the call.
+
+- **Test 5 (reduce to zero then again)**: Uses `reduce_usage` N times in a loop (N = initial `usage_count`), then asserts the (N+1)-th call panics. The `#[should_panic]` attribute applies to the final call.
+
+- **Test 8 (insufficient balance)**: Does not mint any tokens for the payer before calling `topup_subscription`. The MockToken `transfer` will panic when the sender has insufficient balance.
+
+**Pattern for each test** (following repo conventions):
+
+```rust
+use crate::test_utils::{create_test_group, mint_tokens, setup_test_env};
+use crate::AutoShareContractClient;
+use soroban_sdk::{testutils::Address as _, Address, BytesN};
+
+#[test]
+fn test_topup_inactive_group_succeeds() {
+ let test_env = setup_test_env();
+ let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
+ let creator = test_env.users.get(0).unwrap().clone();
+ let token = test_env.mock_tokens.get(0).unwrap().clone();
+
+ let id = create_test_group(&test_env.env, &test_env.autoshare_contract, &creator,
+ &soroban_sdk::Vec::new(&test_env.env), 5, &token);
+ client.deactivate_group(&id, &creator);
+ assert!(!client.is_group_active(&id));
+
+ // top up from the same creator (has balance from create_test_group)
+ mint_tokens(&test_env.env, &token, &creator, 1000);
+ client.topup_subscription(&id, &10u32, &token, &creator);
+
+ let details = client.get(&id);
+ assert_eq!(details.usage_count, 15);
+ assert_eq!(details.total_usages_paid, 15);
+ assert!(!client.is_group_active(&id)); // still inactive
+}
+```
+
+#### Component: `lib.rs` (registration)
+
+Add to the `mod tests` block:
+
+```rust
+#[path = "../tests/subscription_edge_cases_test.rs"]
+mod subscription_edge_cases_test;
+```
+
+### Data Models
+
+No new data structures. Tests use `AutoShareDetails` fields `usage_count` and `total_usages_paid` directly.
+
+### Error Handling
+
+Tests rely on Soroban's `#[should_panic]` attribute. Where possible, test names and comments call out the specific `Error` variant expected (e.g., `NotFound`, `NoUsagesRemaining`) so future developers can tighten assertions if the SDK exposes typed panic matching.
+
+### Testing Strategy
+
+**Unit Testing Approach**: Each test is a `#[test]` function that exercises one scenario. Success tests use direct `assert_eq!` / `assert!` on retrieved state. Panic tests use `#[should_panic]`.
+
+**Property-Based Testing Approach**: Not applicable — the scenarios are specific boundary conditions, not universal properties across arbitrary inputs.
+
+**Integration Testing Approach**: The new tests are integrated into the existing `cargo test --workspace --all-features` invocation in CI with no CI changes needed for this task.
+
+---
+
+## Correctness Properties
+
+*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
+
+### Property 1: Topup accumulates usage counts correctly
+
+For any existing group with initial `usage_count = U` and `total_usages_paid = P`, after calling `topup_subscription` with `additional_usages = N`, the group's `usage_count` becomes `U + N` and `total_usages_paid` becomes `P + N`.
+
+**Validates: Requirements 11.2, 11.3, 13.3, 13.4**
+
+### Property 2: Topup succeeds regardless of group active status
+
+For any existing group (whether active or inactive), `topup_subscription` with a valid token and sufficient payer balance succeeds and updates `usage_count` and `total_usages_paid`.
+
+**Validates: Requirements 10.2, 10.3, 10.4, 10.5**
+
+### Property 3: reduce_usage terminates at zero
+
+For any group with `usage_count = N`, after exactly N calls to `reduce_usage` the count reaches zero, and the (N+1)-th call raises `NoUsagesRemaining`.
+
+**Validates: Requirements 12.2, 12.3**
+
+### Property 4: Gas baselines are non-trivially positive
+
+For any measured contract function, `cpu_insns > 0` and `mem_bytes > 0` after a successful call, confirming the budget measurement is operational.
+
+**Validates: Requirements 4.6**
+
+### Property 5: Snapshot regression is bounded
+
+For any contract function whose baseline is recorded in `autoshare.json`, re-measuring on the same code produces a value within 110 % of the stored baseline.
+
+**Validates: Requirements 6.2, 6.3**
diff --git a/.kiro/specs/notify-chain-improvements/requirements.md b/.kiro/specs/notify-chain-improvements/requirements.md
new file mode 100644
index 0000000..30addd9
--- /dev/null
+++ b/.kiro/specs/notify-chain-improvements/requirements.md
@@ -0,0 +1,227 @@
+# Requirements Document
+
+## Introduction
+
+This document captures the formal requirements for three independent improvements to the Notify-Chain repository. The improvements address a credential-management gap in the dashboard, introduce gas-consumption tracking for the AutoShare Soroban contract, and expand the Rust test suite with subscription edge-case coverage.
+
+Each requirement group is independently deliverable and can be merged as a separate pull request.
+
+---
+
+## Glossary
+
+- **AutoShare Contract**: The Soroban/Rust smart contract located in `contract/contracts/hello-world/`.
+- **Dashboard**: The React/Vite frontend located in `dashboard/`.
+- **Listener**: The Node.js/TypeScript off-chain event listener located in `listener/`.
+- **Gas Snapshot**: A JSON file containing baseline CPU instruction and memory byte counts for measured contract functions.
+- **Budget API**: The Soroban testutils `env.budget()` interface that exposes `cpu_insns_consumed()` and `mem_bytes_consumed()` after a contract call.
+- **Group**: An AutoShare subscription group identified by a `BytesN<32>` ID; has `usage_count` (remaining) and `total_usages_paid` (cumulative) fields.
+- **Topup**: A call to `topup_subscription` that increases a group's `usage_count` and `total_usages_paid` by `additional_usages`.
+- **Reduce**: A call to `reduce_usage` that decrements a group's `usage_count` by 1.
+- **CI**: The GitHub Actions continuous integration pipeline defined in `.github/workflows/ci.yml`.
+
+---
+
+## Requirements
+
+### Requirement 1: Prevent Accidental `.env` Commits in the Dashboard
+
+**User Story:** As a contributor, I want the dashboard directory to exclude `.env` files from version control, so that I cannot accidentally commit secrets by running a standard `git add .`.
+
+#### Acceptance Criteria
+
+1. THE Dashboard `.gitignore` SHALL contain `.env` as an entry so that a file named `.env` created in the `dashboard/` directory is not tracked by Git.
+2. THE Dashboard `.gitignore` SHALL contain `.env.local` and `.env.*.local` as entries to cover common Vite-flavored environment file variants.
+
+---
+
+### Requirement 2: Complete the Listener `.env.example`
+
+**User Story:** As an operator setting up a new Notify-Chain deployment, I want the listener's example environment file to document every variable the listener reads, so that I do not have to read TypeScript source code to find missing configuration keys.
+
+#### Acceptance Criteria
+
+1. THE `listener/.env.example` SHALL contain a documented entry for `DISCORD_WEBHOOK_ID` that explains it is the numeric webhook identifier required alongside `DISCORD_WEBHOOK_URL`.
+2. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_ENABLED` with a default value of `true` and a description of its effect.
+3. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_WINDOW_MS` with a default value of `60000` and a unit annotation (milliseconds).
+4. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_MAX_REQUESTS` with a default value of `60` and a description.
+5. THE `listener/.env.example` SHALL contain a documented entry for `RATE_LIMIT_CLIENT_OVERRIDES` with a default value of `{}` and a JSON-format example showing the per-client override schema.
+
+---
+
+### Requirement 3: Add a Configuration Reference to the Root README
+
+**User Story:** As a new contributor, I want a single place in the repository to find every environment variable for both the listener and the dashboard, so that I can configure my local development environment without reading multiple source files.
+
+#### Acceptance Criteria
+
+1. THE root `README.md` SHALL contain a section headed `## Configuration / Environment Variables`.
+2. WITHIN the Configuration section, THE `README.md` SHALL document all environment variables read by the Listener, including each variable's name, type, default value, and a short description.
+3. WITHIN the Configuration section, THE `README.md` SHALL document all environment variables read by the Dashboard, including each variable's name, type, default value, and a short description.
+4. WHEN a variable has no default (i.e., it is required), THE `README.md` SHALL mark it as required rather than listing a default.
+
+---
+
+### Requirement 4: Gas Snapshot Test Module
+
+**User Story:** As a contract developer, I want automated tests that measure the CPU instruction and memory byte cost of critical contract functions, so that I have a reproducible performance baseline.
+
+#### Acceptance Criteria
+
+1. THE AutoShare Contract SHALL have a test module at `contract/contracts/hello-world/src/tests/gas_snapshot_test.rs` that measures gas usage using the Soroban Budget API.
+2. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `create` function under the baseline scenario (1 group, 10 usages, mock token, no members).
+3. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `topup_subscription` function under the baseline scenario (existing group, 10 additional usages, same token).
+4. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `update_members` function under the baseline scenario (group with 3 members replacing an empty list).
+5. THE gas snapshot test module SHALL measure `cpu_insns_consumed()` and `mem_bytes_consumed()` for the `withdraw` function under the baseline scenario (admin withdraws 100 tokens to a recipient).
+6. WHEN a gas snapshot test runs, THE test SHALL assert that `cpu_insns_consumed() > 0` and `mem_bytes_consumed() > 0`, confirming that measurement is active.
+7. THE gas snapshot test module SHALL print each measurement to stdout in the format `FUNCTION: cpu=NNN mem=NNN` so that CI scripts can parse the values.
+8. THE `lib.rs` test module block SHALL register the gas snapshot test module with `#[path = "../tests/gas_snapshot_test.rs"] mod gas_snapshot_test;`.
+
+---
+
+### Requirement 5: Gas Snapshot Baseline File
+
+**User Story:** As a CI engineer, I want a machine-readable JSON file that records the baseline gas costs, so that the regression gate has a stable reference to compare against.
+
+#### Acceptance Criteria
+
+1. THE repository SHALL contain a gas snapshot file at `contract/contracts/hello-world/gas-snapshots/autoshare.json`.
+2. THE snapshot file SHALL contain a top-level `"version"` field set to `1`.
+3. THE snapshot file SHALL contain a `"functions"` object with entries for `"create"`, `"topup_subscription"`, `"update_members"`, and `"withdraw"`.
+4. EACH function entry in the snapshot file SHALL contain `"cpu_insns"` and `"mem_bytes"` numeric fields and a `"description"` string field.
+5. WHEN the snapshot values are zero (initial skeleton), THE CI regression gate SHALL skip comparison and emit a warning rather than failing.
+
+---
+
+### Requirement 6: CI Gas Regression Gate
+
+**User Story:** As a maintainer, I want CI to block merges that increase any function's gas cost by more than 10 %, so that performance regressions are caught before they reach the main branch.
+
+#### Acceptance Criteria
+
+1. THE `.github/workflows/ci.yml` SHALL contain a step in the `rust` job that re-runs only the gas snapshot tests after the main `cargo test` step.
+2. WHEN the gas regression step runs and the snapshot file contains non-zero baselines, THE CI step SHALL parse the printed measurements and compare each value against the corresponding baseline.
+3. IF any measured `cpu_insns` or `mem_bytes` value exceeds its snapshot baseline by more than 10 %, THEN THE CI step SHALL exit with a non-zero status code and print which function exceeded its budget.
+4. IF the snapshot file contains only zero baseline values, THEN THE CI step SHALL print a warning and exit with status zero (skip regression check).
+5. THE CI regression gate step SHALL use only standard Unix tools (`jq`, `awk`, `bash`) available on the `ubuntu-latest` runner without additional installation steps.
+
+---
+
+### Requirement 7: Gas Usage Documentation
+
+**User Story:** As a developer unfamiliar with Soroban resource accounting, I want a human-readable document that explains what each function costs and how to update the baselines, so that I can reason about the contract's on-chain economics.
+
+#### Acceptance Criteria
+
+1. THE repository SHALL contain a `GAS_USAGE.md` file at the repository root.
+2. THE `GAS_USAGE.md` SHALL contain a section explaining Soroban CPU instruction and memory byte metering in plain language.
+3. THE `GAS_USAGE.md` SHALL contain a table of the four measured functions listing their baseline `cpu_insns`, `mem_bytes`, and the scenario used for measurement.
+4. THE `GAS_USAGE.md` SHALL contain instructions for how to update the snapshot (run the gas snapshot tests with `--nocapture`, copy printed values into `autoshare.json`).
+5. THE `GAS_USAGE.md` SHALL document the 10 % regression tolerance policy.
+
+---
+
+### Requirement 8: Duplicate Create Error-Type Test
+
+**User Story:** As a contract developer, I want a test that verifies the exact error type returned when `create` is called with a duplicate group ID, so that regressions in error classification are caught.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_duplicate_create_error_type` in `subscription_edge_cases_test.rs` that calls `create` twice with the same `BytesN<32>` ID.
+2. WHEN `create` is called with a duplicate ID, THE test SHALL assert that the call panics (corresponding to `Error::AlreadyExists`).
+
+---
+
+### Requirement 9: Topup on Non-Existent Group Test
+
+**User Story:** As a contract developer, I want a test that verifies `topup_subscription` panics when the target group ID does not exist, so that the NotFound guard is confirmed.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_topup_nonexistent_group` that calls `topup_subscription` with a `BytesN<32>` ID that was never passed to `create`.
+2. WHEN `topup_subscription` is called with an unknown group ID, THE test SHALL assert that the call panics.
+
+---
+
+### Requirement 10: Topup on Inactive Group Test
+
+**User Story:** As a contract developer, I want a test that confirms `topup_subscription` intentionally succeeds on a deactivated group and correctly updates usage counts, so that this permissive behaviour is explicitly documented as tested.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_topup_inactive_group_succeeds` that creates a group, deactivates it, and then calls `topup_subscription` on it.
+2. WHEN `topup_subscription` is called on an inactive group with a funded payer, THE test SHALL assert that the call succeeds.
+3. AFTER a successful topup on an inactive group, THE test SHALL assert that `usage_count` equals the initial count plus the additional usages.
+4. AFTER a successful topup on an inactive group, THE test SHALL assert that `total_usages_paid` equals the initial total plus the additional usages.
+5. AFTER a successful topup on an inactive group, THE test SHALL assert that `is_group_active` still returns `false`.
+
+---
+
+### Requirement 11: Multiple Sequential Topups Test
+
+**User Story:** As a contract developer, I want a test that performs multiple consecutive topups and verifies cumulative accounting, so that off-by-one or reset bugs are detected.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_multiple_sequential_topups` that calls `topup_subscription` at least three times on the same group.
+2. AFTER each topup, THE test SHALL assert that `usage_count` and `total_usages_paid` equal the expected cumulative sum.
+3. AFTER all topups, THE test SHALL assert that `total_usages_paid` equals the initial usages plus the sum of all topup amounts.
+
+---
+
+### Requirement 12: Reduce to Zero Then Over-Reduce Test
+
+**User Story:** As a contract developer, I want a test that reduces a group's usage count to zero and then attempts one more reduction, so that the NoUsagesRemaining guard is confirmed.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_reduce_to_zero_then_reduce_again` that creates a group with a small fixed `usage_count` (e.g., 3) and calls `reduce_usage` exactly that many times.
+2. AFTER reducing to zero, THE test SHALL assert that `get_remaining_usages` returns `0`.
+3. WHEN `reduce_usage` is called one more time after the count reaches zero, THE test SHALL assert that the call panics.
+
+---
+
+### Requirement 13: Large Usage Count Arithmetic Test
+
+**User Story:** As a contract developer, I want a test that tops up with a very large number of usages (e.g., 10 000) and verifies that the arithmetic is exact, so that potential integer overflow or truncation bugs are caught.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_large_usage_count` that calls `topup_subscription` with `additional_usages = 10_000`.
+2. THE test SHALL mint sufficient tokens to the payer to cover `10_000 * usage_fee` before calling `topup_subscription`.
+3. AFTER the topup, THE test SHALL assert that `usage_count` equals exactly `initial_count + 10_000`.
+4. AFTER the topup, THE test SHALL assert that `total_usages_paid` equals exactly `initial_total + 10_000`.
+
+---
+
+### Requirement 14: Third-Party Payer Topup Test
+
+**User Story:** As a contract developer, I want a test that verifies any funded address (not just the group creator) can top up a subscription, so that the permissive payer model is explicitly tested.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_topup_different_payer` that generates a new address distinct from the group creator and mints tokens to that address.
+2. WHEN `topup_subscription` is called by the non-creator payer, THE test SHALL assert that the call succeeds.
+3. AFTER the topup, THE test SHALL assert that `usage_count` and `total_usages_paid` are updated correctly.
+
+---
+
+### Requirement 15: Topup with Insufficient Balance Test
+
+**User Story:** As a contract developer, I want a test that verifies `topup_subscription` panics when the payer has no tokens, so that the token transfer failure path is confirmed.
+
+#### Acceptance Criteria
+
+1. THE test suite SHALL contain a test named `test_topup_insufficient_balance` that generates a payer address without minting any tokens to it.
+2. WHEN `topup_subscription` is called by the zero-balance payer, THE test SHALL assert that the call panics.
+
+---
+
+### Requirement 16: Register Subscription Edge-Case Module in `lib.rs`
+
+**User Story:** As a contributor, I want the subscription edge-case tests to be compiled and run as part of `cargo test --workspace --all-features`, so that CI executes them automatically.
+
+#### Acceptance Criteria
+
+1. THE `lib.rs` `mod tests` block SHALL contain the declaration `#[path = "../tests/subscription_edge_cases_test.rs"] mod subscription_edge_cases_test;`.
+2. WHEN `cargo test --workspace --all-features` is run, THE Command SHALL execute all tests in `subscription_edge_cases_test.rs` without compilation errors.
diff --git a/.kiro/specs/notify-chain-improvements/tasks.md b/.kiro/specs/notify-chain-improvements/tasks.md
new file mode 100644
index 0000000..d8f4630
--- /dev/null
+++ b/.kiro/specs/notify-chain-improvements/tasks.md
@@ -0,0 +1,269 @@
+# Implementation Plan: Notify-Chain Improvements
+
+## Overview
+
+Three independent task groups, each committable to its own branch. Tasks within each group build on one another; groups are fully independent.
+
+---
+
+## Task Dependency Graph
+
+```json
+{
+ "waves": [
+ {
+ "wave": 1,
+ "tasks": ["1", "5", "11"],
+ "description": "First task of each independent group — can all start in parallel"
+ },
+ {
+ "wave": 2,
+ "tasks": ["2", "6", "12"],
+ "description": "Builds on wave 1 within each group"
+ },
+ {
+ "wave": 3,
+ "tasks": ["3", "7", "13"],
+ "description": "Builds on wave 2 within each group"
+ },
+ {
+ "wave": 4,
+ "tasks": ["4", "8", "14"],
+ "description": "Checkpoints and final registrations"
+ },
+ {
+ "wave": 5,
+ "tasks": ["9", "15"],
+ "description": "Documentation and final verification"
+ },
+ {
+ "wave": 6,
+ "tasks": ["10"],
+ "description": "Task Group 2 final checkpoint"
+ }
+ ]
+}
+```
+
+The three groups (Tasks 1–4, Tasks 5–10, Tasks 11–15) are fully independent and can be executed in parallel on separate branches.
+
+---
+
+## Tasks
+
+
+
+
+
+- [ ] 1. Add `.env` to `dashboard/.gitignore`
+ - Open `dashboard/.gitignore` and append `.env`, `.env.local`, and `.env.*.local` entries.
+ - Verify that creating a `dashboard/.env` file and running `git status` does not show the file as untracked.
+ - _Requirements: 1.1, 1.2_
+
+- [ ] 2. Complete `listener/.env.example` with missing variables
+ - Add a `# Discord Webhook Configuration` comment block with `DISCORD_WEBHOOK_ID=` including an inline description explaining it is the numeric webhook ID paired with `DISCORD_WEBHOOK_URL`.
+ - Add a `# Rate Limiting Configuration` comment block containing:
+ - `RATE_LIMIT_ENABLED=true` with description
+ - `RATE_LIMIT_WINDOW_MS=60000` with unit annotation (milliseconds)
+ - `RATE_LIMIT_MAX_REQUESTS=60` with description
+ - `RATE_LIMIT_CLIENT_OVERRIDES={}` with a JSON schema example comment showing `{"client-id": {"maxRequests": 30, "windowMs": 60000}}`
+ - Verify the file parses correctly (each line is a valid `KEY=value` or comment).
+ - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_
+
+- [ ] 3. Add Configuration reference section to root `README.md`
+ - Locate or create an appropriate position in the root `README.md` (after the existing introduction/setup sections).
+ - Add `## Configuration / Environment Variables` heading.
+ - Add a `### Listener (\`listener/.env\`)` sub-section with a Markdown table containing columns: Variable, Type, Default, Required, Description — populated from all variables in `listener/.env.example`.
+ - Add a `### Dashboard (\`dashboard/.env\`)` sub-section with the same table format — populated from all variables in `dashboard/.env.example`.
+ - Mark variables with no default (e.g. `CONTRACT_ADDRESSES`, `DISCORD_WEBHOOK_URL`, `DISCORD_WEBHOOK_ID`) as Required = Yes.
+ - _Requirements: 3.1, 3.2, 3.3, 3.4_
+
+- [ ] 4. Checkpoint — Task 1 complete
+ - Confirm `dashboard/.gitignore` includes `.env`.
+ - Confirm `listener/.env.example` includes all five new variables.
+ - Confirm root `README.md` contains the Configuration section with both sub-sections.
+ - Ensure all tests pass, ask the user if questions arise.
+
+
+
+
+
+- [ ] 5. Create the gas snapshot baseline JSON skeleton
+ - Create directory `contract/contracts/hello-world/gas-snapshots/`.
+ - Create `contract/contracts/hello-world/gas-snapshots/autoshare.json` with the following skeleton (all numeric values set to `0`):
+ ```json
+ {
+ "version": 1,
+ "functions": {
+ "create": { "cpu_insns": 0, "mem_bytes": 0, "description": "Create a new AutoShare group (10 usages, no members)" },
+ "topup_subscription": { "cpu_insns": 0, "mem_bytes": 0, "description": "Top up existing group with 10 additional usages" },
+ "update_members": { "cpu_insns": 0, "mem_bytes": 0, "description": "Set 3 members with equal percentage split" },
+ "withdraw": { "cpu_insns": 0, "mem_bytes": 0, "description": "Admin withdraws 100 tokens to recipient" }
+ }
+ }
+ ```
+ - _Requirements: 5.1, 5.2, 5.3, 5.4_
+
+- [ ] 6. Implement `gas_snapshot_test.rs`
+ - Create `contract/contracts/hello-world/src/tests/gas_snapshot_test.rs`.
+ - Add imports: `use crate::test_utils::{create_test_group, mint_tokens, setup_test_env}; use crate::AutoShareContractClient; use soroban_sdk::{testutils::Address as _, Address, BytesN, String, Vec};`
+ - Implement four `#[test]` functions following the pattern below. Each test MUST:
+ 1. Call `test_env.env.budget().reset_unlimited()` immediately before the contract call being measured.
+ 2. Perform the contract call.
+ 3. Read `test_env.env.budget().cpu_insns_consumed()` and `test_env.env.budget().mem_bytes_consumed()`.
+ 4. Assert both values are `> 0`.
+ 5. Print `println!("SNAPSHOT {name}: cpu={cpu} mem={mem}");` so the CI gate can parse output.
+ - **`snapshot_create`**: mint 10 000 tokens to creator, call `client.create(...)` with 10 usages, measure budget.
+ - **`snapshot_topup_subscription`**: use `create_test_group(...)` to set up a group, mint extra tokens to creator, call `client.topup_subscription(...)` with 10 additional usages, measure budget.
+ - **`snapshot_update_members`**: create a group (0 members), build a `Vec` with 3 members at 34/33/33 %, call `client.update_members(...)`, measure budget.
+ - **`snapshot_withdraw`**: create a group (funds land in contract), call `client.withdraw(admin, token, 100, recipient)`, measure budget.
+ - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7_
+
+- [ ]* 6.1 Verify gas snapshot tests compile and run
+ - Run `cargo test --all-features gas_snapshot -- --nocapture` in `contract/`.
+ - Confirm all four tests pass and each prints a `SNAPSHOT ...: cpu=NNN mem=NNN` line with non-zero values.
+ - Copy the printed values into `autoshare.json` (replacing the `0` skeletons) and commit.
+ - _Requirements: 4.6, 4.7, 5.4_
+
+- [ ] 7. Register `gas_snapshot_test` module in `lib.rs`
+ - In `contract/contracts/hello-world/src/lib.rs`, inside the `mod tests { ... }` block, add:
+ ```rust
+ #[path = "../tests/gas_snapshot_test.rs"]
+ mod gas_snapshot_test;
+ ```
+ - Run `cargo build --all-features` in `contract/` to confirm no compilation errors.
+ - _Requirements: 4.8_
+
+- [ ] 8. Add CI gas regression gate step to `.github/workflows/ci.yml`
+ - After the existing `Run tests` step in the `rust` job, add a new step named `Gas regression check`.
+ - The step runs in `working-directory: contract`.
+ - Shell script logic:
+ 1. Run `cargo test --all-features gas_snapshot -- --nocapture 2>&1` and capture output.
+ 2. Check whether the snapshot file contains all-zero baselines; if so, print a warning and exit 0.
+ 3. For each of the four function names, parse the `cpu_insns` and `mem_bytes` from the captured output using `grep` and `awk`.
+ 4. Read the baseline values from `contracts/hello-world/gas-snapshots/autoshare.json` using `jq`.
+ 5. For each metric, compute `threshold = baseline * 110 / 100` using integer arithmetic in `awk`.
+ 6. If any measured value exceeds its threshold, print the function name and values, then exit 1.
+ - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
+
+- [ ] 9. Create `GAS_USAGE.md` at the repository root
+ - Create `GAS_USAGE.md` with the following sections:
+ - `## Overview` — 2–3 sentences explaining Soroban CPU instruction and memory byte metering (what they measure, where limits are enforced).
+ - `## Function Baselines` — a Markdown table with columns: Function, Scenario, CPU Instructions, Memory Bytes; rows for `create`, `topup_subscription`, `update_members`, `withdraw`. Populate from values committed in task 6.1.
+ - `## How to Update the Snapshot` — step-by-step: run `cargo test --all-features gas_snapshot -- --nocapture`, note printed values, update `autoshare.json`.
+ - `## Regression Policy` — document the 10 % tolerance and how the CI gate enforces it.
+ - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_
+
+- [ ] 10. Checkpoint — Task 2 complete
+ - Confirm snapshot JSON has non-zero values.
+ - Confirm `gas_snapshot_test.rs` is registered in `lib.rs`.
+ - Confirm CI `ci.yml` has the new regression step.
+ - Confirm `GAS_USAGE.md` exists with all required sections.
+ - Ensure all tests pass, ask the user if questions arise.
+
+
+
+
+
+- [ ] 11. Create `subscription_edge_cases_test.rs` with error-path tests
+ - Create `contract/contracts/hello-world/src/tests/subscription_edge_cases_test.rs`.
+ - Add standard imports at the top of the file:
+ ```rust
+ use crate::test_utils::{create_test_group, mint_tokens, setup_test_env};
+ use crate::AutoShareContractClient;
+ use soroban_sdk::{testutils::Address as _, Address, BytesN, Vec};
+ ```
+ - Implement the following `#[should_panic]` tests:
+ - **`test_duplicate_create_error_type`**: call `create_test_group` twice with the same manually-constructed `BytesN<32>` ID (e.g., `[1u8; 32]`). The second call must panic. Add comment `// Expects Error::AlreadyExists`.
+ - **`test_topup_nonexistent_group`**: construct a random `BytesN<32>` that was never used in `create`, call `client.topup_subscription(...)` on it. Must panic. Add comment `// Expects Error::NotFound`.
+ - **`test_topup_insufficient_balance`**: create a group normally, generate a new `payer` address, do NOT mint any tokens, call `client.topup_subscription(...)` with that payer. Must panic. Add comment `// Expects token transfer failure`.
+ - _Requirements: 8.1, 8.2, 9.1, 9.2, 15.1, 15.2_
+
+- [ ]* 11.1 Write property test for topup accumulation (Property 1)
+ - **Property 1: Topup accumulates usage counts correctly**
+ - In the same file, add a parameterized helper `assert_topup_accumulates(initial: u32, topup: u32)` that:
+ 1. Creates a group with `initial` usages.
+ 2. Mints enough tokens.
+ 3. Calls `topup_subscription` with `topup` usages.
+ 4. Asserts `usage_count == initial + topup` and `total_usages_paid == initial + topup`.
+ - Call it from a `#[test]` named `test_topup_accumulation_property` with representative pairs: `(1, 1)`, `(5, 10)`, `(100, 200)`, `(1, 10_000)`.
+ - **Validates: Requirements 11.2, 11.3, 13.3, 13.4**
+
+- [ ] 12. Implement success-path edge-case tests
+ - In `subscription_edge_cases_test.rs`, implement the following success-path `#[test]` functions:
+ - **`test_topup_inactive_group_succeeds`**:
+ 1. Create a group with `initial_usages = 5`.
+ 2. Call `client.deactivate_group(...)`.
+ 3. Assert `!client.is_group_active(...)`.
+ 4. Mint extra tokens to `creator`.
+ 5. Call `client.topup_subscription(id, 10, token, creator)`.
+ 6. Assert `client.get(&id).usage_count == 15`.
+ 7. Assert `client.get(&id).total_usages_paid == 15`.
+ 8. Assert `!client.is_group_active(...)` (still inactive).
+ - **`test_multiple_sequential_topups`**:
+ 1. Create a group with `initial_usages = 5`.
+ 2. Perform three topups of `10`, `20`, and `30` usages (mint tokens before each).
+ 3. After each topup, assert `usage_count` and `total_usages_paid` equal the expected cumulative sum.
+ 4. After all topups, assert `total_usages_paid == 5 + 10 + 20 + 30 == 65`.
+ - **`test_large_usage_count`**:
+ 1. Create a group with `initial_usages = 1`.
+ 2. Mint `10_000 * usage_fee + buffer` tokens to the creator.
+ 3. Call `client.topup_subscription(id, 10_000, token, creator)`.
+ 4. Assert `usage_count == 10_001` and `total_usages_paid == 10_001`.
+ - **`test_topup_different_payer`**:
+ 1. Create a group with `initial_usages = 5`.
+ 2. Generate a new `payer` address distinct from `creator`.
+ 3. Mint `10 * usage_fee` tokens to `payer`.
+ 4. Call `client.topup_subscription(id, 10, token, payer)`.
+ 5. Assert `usage_count == 15` and `total_usages_paid == 15`.
+ - _Requirements: 10.1–10.5, 11.1–11.3, 13.1–13.4, 14.1–14.3_
+
+- [ ]* 12.1 Write property test for inactive-group topup (Property 2)
+ - **Property 2: Topup succeeds regardless of group active status**
+ - Add a `#[test]` named `test_inactive_topup_property` that calls `assert_topup_accumulates` helper (from task 11.1) after deactivating the group, with pairs: `(5, 5)`, `(1, 100)`.
+ - Assert `is_group_active` returns `false` after topup in both cases.
+ - **Validates: Requirements 10.2, 10.3, 10.4, 10.5**
+
+- [ ] 13. Implement `test_reduce_to_zero_then_reduce_again`
+ - In `subscription_edge_cases_test.rs`, add:
+ - **`test_reduce_to_zero_then_reduce_again`** (success part, `#[test]`):
+ 1. Create a group with `initial_usages = 3`.
+ 2. Call `client.reduce_usage(...)` three times in a loop.
+ 3. Assert `client.get_remaining_usages(...) == 0`.
+ - **`test_reduce_below_zero_panics`** (`#[test] #[should_panic]`):
+ 1. Create a group with `initial_usages = 3`.
+ 2. Call `client.reduce_usage(...)` four times. The fourth call must panic.
+ 3. Add comment `// Expects Error::NoUsagesRemaining`.
+ - _Requirements: 12.1, 12.2, 12.3_
+
+- [ ]* 13.1 Write property test for reduce_usage boundary (Property 3)
+ - **Property 3: reduce_usage terminates at zero**
+ - Add a `#[test]` named `test_reduce_terminates_at_zero_property` that runs for `initial_usages` in `[1, 2, 5, 10]`:
+ 1. For each value N, create a fresh group with N usages.
+ 2. Call `reduce_usage` N times.
+ 3. Assert `get_remaining_usages == 0`.
+ - Note: the panic test (over-reduce) remains as `test_reduce_below_zero_panics` from task 13.
+ - **Validates: Requirements 12.2, 12.3**
+
+- [ ] 14. Register `subscription_edge_cases_test` module in `lib.rs`
+ - In `contract/contracts/hello-world/src/lib.rs`, inside the `mod tests { ... }` block, add:
+ ```rust
+ #[path = "../tests/subscription_edge_cases_test.rs"]
+ mod subscription_edge_cases_test;
+ ```
+ - Run `cargo build --all-features` in `contract/` to confirm no compilation errors.
+ - _Requirements: 16.1_
+
+- [ ] 15. Final checkpoint — Task 3 complete
+ - Run `cargo test --workspace --all-features` in `contract/` and confirm all tests in `subscription_edge_cases_test` pass.
+ - Confirm the module is registered in `lib.rs`.
+ - Ensure all tests pass, ask the user if questions arise.
+
+---
+
+## Notes
+
+- Tasks marked with `*` are optional and can be skipped for a faster MVP; the non-starred tests in the same group still provide solid coverage.
+- Each task group (1–4, 5–10, 11–15) maps to a separate branch and PR.
+- Property tests in this spec are implemented as parameterised `#[test]` functions with multiple representative inputs rather than a full PBT library, since the Soroban `no_std` environment limits available PBT crates.
+- The gas snapshot values in `autoshare.json` must be populated from a real measurement run before the CI regression gate becomes meaningful; the skeleton file with zeros causes the gate to skip (per Requirement 6.4).