1. The Eskom app
Your settlement can only be as current as the tariff and network reference data behind it. The Eskom seat is where you publish those inputs; it is not where you negotiate or settle a contract.
Follow along. Seat:
eskom@webill.net(Eskom; passwordsupersecret). Path: sign in → Tariffs. You should see: a tariff table with charge counts, Add New Tariff, and a per-row Permutate action.
Where this is taught. The meanings of tariff, rate, DLF, TLF, and UoS are owned by V2 §8. Their settlement application is owned by V8 §4.
1.1 Know the boundary before you edit
The landing route points at Virtual Contracts.
/eskom/virtual-contracts is the VWA register — registered
Virtual Wheeling accounts, enrolled meters, the month’s refund
obligation, and 45-day aging. It is taught in V17 §9.
/eskom/wheeling-contracts is still a placeholder. Create
and sign a per-contract agreement in V7,
then read its monetary result in V8.
1.2 Build a tariff in the order the screen expects
Add New Tariff creates a named shell. Open that tariff’s Charges screen to add a Consumption Charge or Service Charge. Each form asks for Name, Index, Frequency, Season, TOU, Denomination, and Status; complete the commercial sheet before activating it. Consumption indices are energy based, such as KWH or MWH. Service lines use their own index basis, such as POD or FLAT, and must not be substituted for an energy rate.
| Screen | Path | You maintain | Expected UI |
|---|---|---|---|
| Tariffs | /eskom/tariffs |
tariff shells and permutations | table, drill-down actions |
| Charges | /eskom/tariffs/:id/charges |
consumption/service charge definitions | two add actions and a charge list |
| Rates | /eskom/tariffs/:id/rates |
Season · TOU · Description values | inline value field and Save |
Permutate creates missing HIGH/LOW × PEAK/STANDARD/OFF rate shells for eligible charges. It is deliberately disabled once every charge has already been permutated or activated: disabled means there is nothing left to generate, not that the page failed. A charge marked INACTIVE remains reference data but does not price settlement.
Careful. Permutate produces empty rate positions, not approved prices. Enter the published value in each inline row and press Save before marking a charge ACTIVE.
1.3 Worked example — roll the tariff forward safely
Data. full_demo · seed 42 · stage S02 (tariffs and losses) · illustrative 2026/27 rate sheet.
| Input | Value |
|---|---|
| New tariff | Megaflex 2026/27 |
| Consumption line | Energy, KWH, monthly, HIGH/PEAK |
| Service line | Network access, POD, monthly, FLAT |
| Energy rate | 120.0c/kWh |
| Service rate | R 450.00/POD |
- On Tariffs, choose Add New Tariff
and save
Megaflex 2026/27. - Open Charges and add the energy line. Set its denomination to cents, its index to KWH, its frequency to monthly, then leave it INACTIVE while you complete the rate grid.
- Add the service line using POD and FLAT. It remains a separate
charge: R1:
16,142 kWh × 120.0c ÷ 100 = R 19,370.40energy; R2:1 POD × R 450.00 = R 450.00service. - Return to the tariff table and choose Permutate. Open Rates, enter each approved Season/TOU value, and use the row’s inline Save.
- Activate only the charge lines whose full grid is approved. Check the site tariff selection afterward; settlement reads published data, never invents a missing rate.
Rates sample view
| Season 1 | TOU 2 | Description | Value 3 |
|---|---|---|---|
| HIGH | PEAK | Energy | 120.0c · Save |
- 1Season selects the published season.
- 2TOU identifies the interval band.
- 3Value is not live until the inline save succeeds.
What you see on screen is a saved rate row and an ACTIVE charge; this is the pattern to expect on your own page. The resulting settlement line belongs in V8 §7, not in this editor.
1.4 Maintain losses and UoS separately
Open Wheeling Charges at
/eskom/loss-factors. The screen has distinct tables and
distinct add actions: Add DLF, Add
TLF, Add UoS, and Add Levy.
Enter the field set shown by each form—DLF by voltage tier, TLF by zone
and distance band, UoS by category and charge definition, non-bypassable
levies by electrification-subsidy or regulatory-levy category and a
c/kWh rate—rather than copying one factor into another table. The levy
table’s Non-bypassable column is fixed
Yes; it is not a checkbox.
| Reference table | Operational question it answers |
|---|---|
| DLF | What distribution factor applies at this voltage tier? |
| TLF | What transmission factor applies for this zone/distance band? |
| UoS | What network-use charge applies to this category? |
| Non-bypassable levies | What national levy (electrification subsidy / NERSA regulatory levy) applies on this date? |
Follow along. Seat:
eskom@webill.net(Eskom). Path: Wheeling Charges → choose Add DLF, Add TLF, Add UoS, or Add Levy. You should see: a form for that specific reference type and its row in the matching table after save.
Do not diagnose a final statement by changing historical reference
rows. Trace the period and its status first: settlement moves
DRAFT → PRELIMINARY → FINAL, and a FINAL result needs its
explicit correction path.
Where this is taught. Statuses and controlled re-settlement are taught in V8 §11. The municipal mirror is §2.
1.5 Your publication check before settlement
Before a new billing period, inspect one active charge from the tariff list through its rate grid and one network row through the loss-factor screen. Confirm the denomination is compatible with the number you entered: cents per kWh and rand per POD are not interchangeable. Record the published source and effective operational decision outside this screen according to your governance process.
| Check | Screen evidence |
|---|---|
| Tariff name and charge | visible in the tariff/charge table |
| Seasonal rate | saved Season, TOU, Description, and value row |
| Charge availability | Status is ACTIVE only when ready |
| Network reference | DLF, TLF, UoS, or a non-bypassable levy exists in its own table |
If the expected price on a settlement differs, trace the contract’s selected tariff, interval classification, and status before changing a rate. That sequence tells you whether the issue is configuration, a period correction, or a misunderstanding of the ledger.
1.6 The operator billing portal
/operator is a new persona — not
/eskom and not /so. Path: landing page →
Eskom door → sidebar Billing (a POST,
not a bookmark). The issuer chip is Eskom’s legal name. Search the
issued register by month, entity, and organization. Month run approves
and re-issues. Received remittances are the supply-only BRP hat.
B13. Doors are the interim identity story. Production Eskom users will need accounts; this path is what ships until that decision.
B14. Leaving transition (issuing_from)
is an MO/ops act, not a switch on Settings. Until that month, off-takers
see the amount in V15 §13 and
still receive the bill from the utility. The first issued month is the
month on or after issuing_from.
Fuel prices
/eskom/fuel-prices shows effective-dated fuel prices in
R/GJ, their basis and source link. Missing fuels remain missing;
references older than 120 days are marked stale. To record a new price,
close the old series window and create a new sourced row. Overlapping
windows in the same fuel/source series are refused. The seeded April
2026 gas price remains secondary-sourced pending the filed NERSA note.
The diesel derivation uses a rounded R/GJ price, so its reconstructed
R/MWh differs slightly from the original expenditure quotient.
For each quarterly gas update, obtain the filed NERSA/Sasol price notice, verify its effective date and units, close the prior same-source window, and enter the new R/GJ row with the document reference. Do not silently replace the April 2026 secondary source with an assumed official value. For the annual diesel reference, retain the Eskom expenditure and generation figures and the heat-rate basis used to derive R/GJ; document the rounding. Reopen a thermal plant’s dispatch card to check the newly pinned row. The register page is read-only; changes use the reference-data actions.
2. The Munix app
Your sites can appear eligible on a map yet still lack a municipal tariff or charge configuration. Munix is where the municipality publishes its participation state and its local reference data; your session is scoped to that municipality from the moment you sign in.
Follow along. Seat:
<normalised municipality name>@webill.net(Munix; passwordsupersecret). Path: sign in → Configuration. You should see: the municipal status chip, boundary/map information, participation facts, and wheeling charge controls when eligible.
Where this is taught. The map, site eligibility, and municipality object are taught in V2 §9. This chapter teaches the municipality’s publishing screens.
2.1 Set the operational status first
Configuration is /munix/configuration;
/munix redirects there. Choose the chip that accurately
describes current participation: Wheeling,
Pilot, Partial, or
None. This is a published operational fact, not a draft
workflow. Org-facing maps and eligibility banners reflect it when those
pages next load; there is no separate publish button.
| Status chip | What you should communicate operationally |
|---|---|
| Wheeling | municipal wheeling configuration may be maintained |
| Pilot | limited rollout; confirm eligibility in the owning V1 flow |
| Partial | only some capability/coverage is represented |
| None | no wheeling participation is published |
The Embedded wheeling tariff and Monthly Admin Fee controls appear only while the status is Wheeling. Enter the values that the municipality has approved, then choose Save. Changing a map boundary or seeing a map does not create a fee; the charge configuration is its own state.
2.2 Worked example — turn a participating municipality into usable data
Data. full_demo · seed 42 · stage S02 (tariffs and municipal reference data) · illustrative municipal configuration.
| Input | Value |
|---|---|
| Published status | Wheeling |
| Embedded wheeling tariff | 15.0c/kWh |
| Monthly Admin Fee | R 850.00 |
| Delivered interval | 16,142 kWh |
- Open Configuration and verify the current chip before changing it. Select Wheeling only when the municipality is genuinely available for wheeling.
- The wheeling-charge area now appears. Enter
15.0c/kWhfor Embedded wheeling tariff andR 850.00for Monthly Admin Fee, then press Save. - The illustrative monthly network amount is R1:
16,142 kWh × 15.0c ÷ 100 = R 2,421.30; R2:R 2,421.30 + R 850.00 = R 3,271.30. - Re-open Configuration and confirm the saved values. Then open the V1 map in a separate session: the status is what map and eligibility surfaces consume, while the arithmetic itself is applied by settlement.
What you see on screen is the Wheeling chip plus populated, saved charge fields. This is the pattern to expect on your own page; do not expect an issued statement here.
Where this is taught. The monetary line and its ledger destination are taught in V8 §7 and V8 §8.
2.3 Maintain municipal tariffs without confusing ownership
At /munix/tariffs, you follow the same data sequence as
Eskom: add a tariff, add charge definitions, Permutate
missing seasonal/TOU positions, fill inline rates, save, then activate
ready charges. A municipal tariff is available only to sites in that
municipality. It does not replace national loss-factor maintenance,
which remains the Eskom seat’s job.
| Route | Shared implementation | Meaning for you |
|---|---|---|
/munix/tariffs/:id/charges |
Eskom.ChargesLive with :munix |
municipal charge editor |
/munix/tariffs/:id/rates |
Eskom.RatesLive with :munix |
municipal rate editor |
Follow along. Seat: municipality email (Munix). Path: Tariffs → choose a tariff → Charges → Rates. You should see: the same charge/rate editing pattern as Eskom, inside the Munix shell and scoped to your municipality.
Use Permutate before you fill the grid. It is disabled when all eligible charges have already been permutated or activated, which is an expected completed state. A saved INACTIVE charge remains unavailable to pricing until you activate it.
2.4 Know what is deliberately absent
/munix/wheeling-contracts and
/munix/wheeling-charges are placeholders. They are not
alternate contract or charge registers. The former does not list
agreements; the latter is not the Eskom DLF/TLF/UoS editor.
Careful. “Wheeling” status and the map colour answer whether the municipality is published as participating. They do not prove that a particular site has a valid tariff, a contract, readings, or a FINAL settlement.
Where this is taught. National tariff and loss-factor reference data is in §1; agreements are in V7; settlement proceeds
DRAFT → PRELIMINARY → FINALin V8 §11.
2.5 Your hand-off check
Before telling an organisation that municipal wheeling is ready, make the separate facts visible. Configuration must show the intended status; a Wheeling municipality must have saved charge values where the tariff is required; and the organisation must still complete its site and agreement work. These facts are related, but none is a substitute for another.
| Question | Seat that answers it |
|---|---|
| Is the municipality participating? | Munix Configuration |
| What local tariff and fee apply? | Munix Tariffs / charge configuration |
| Is this site eligible and mapped? | organisation screens in V1 |
| Is there an active agreement? | deal room in V3 |
| What was actually priced? | settlement in V4 |
Careful. Do not use a placeholder route as evidence that an agreement or a charge has been registered. The current placeholders are intentionally honest about the feature boundary.
2.4 Municipal billing
Sidebar Billing POSTs to
/operator/session with this municipality already in the
session. The issuer chip is that municipality’s legal name, never
Eskom’s. The rest of the walk — register, month run, received
remittances, settings — is V15 §13.
B13/B14 apply here as on the Eskom door: doors are interim; transition
flip stays with MO/ops.
3. Background-process catalog
Every scheduled job, enqueue-driven worker, AshOban trigger, long-running sim process and operator mix task — what it does, when it runs, what you see, and what to do if it fails. Feature depth lives in the linked volume; this chapter is the operations index.
How to learn the clock. Narrative, step-by-step process and worked examples for the four rhythms: V1 · The Four Rhythms. Orientation preview: V0 §6. This catalog keeps failure modes, queues and retry behaviour.
Counts at the pinned
config/config.exs. Oban cron entries and distinct workers: see COVERAGE.md Matrix 3. The complete run list is V0 §6; this chapter is the operations index (failure modes, queues, retries). Reconcile against COVERAGE when cron changes.
Schedules below are SAST as configured. Gate times for market workers still come from MarketRules (V9 §1).
3.1 The 24-hour dial
- 00:05 Archive SO instructions
- 01:00 Daily settlement runner
- 02:30 RunNightly (books + BESS inputs)
- 02:00 Weather cycle · 06/09/12/15 nowcast drift
- 03:00 Submit DAM bids · simulated-market DAM
- 06:30 Compute imbalance (D+1)
- 07:20 / 07:50 CAD Trust observer / publisher
- 12:00 Fetch DAM schedule
- 12:30 Refresh SMP price forecast
- */15 Poll SO · 04 / 10 / 16 / 22 IDM monitor
- hourly Expire stale proposals (AshOban)
3.2 The month strip
- 1st 03:00 Monthly settlement · 1st 04:00 Chain reconciliation · 1st 04:45 Certificate accrual
- 1st */3 03:00 Quarterly settlement
- 3rd 07:00 MO settlement-file reconcile
- 4th 04:00 Strategy backcast (prior month)
- 5th 04:00 IPP monthly forecast fan-out
- 20th 05:00 Propose nominations
- Mondays 02:00 Weekly settlement
- daily 00:00 Activate SIGNED physical PPAs on start date
3.3 Cron cards
FetchRates — reference FX publications
The market_data queue runs at 10:45 and 14:00 SAST on
weekdays, with a fallback pass at 17:30 SAST. An existing primary
publication makes the live fetch a no-op. A fallback pass uses the
source policy only when the primary set for that date is absent.
Calendar absence and provider refusals end the job; transient failures
have at most three attempts with 30-second and two-minute waits before
the remaining attempts.
Mocks remain the configured default. Live fetching requires explicit
provider configuration and an effective WB.Fx.SourcePolicy
row. Every publication retains publisher, channel, publication time and
retrieval time. A Friday response to a Sunday request is refused.
mix wb.fx.backfill --from YYYY-MM-DD --to YYYY-MM-DD
requests SARB history through Frankfurter in chunks of at most 90 days;
it refuses while the mock is configured.
Live valuation is not armed by this release. The desk must resolve PD-F8-1 before go-live: a trading-day SARB rate is published after the SAPP day-ahead gate. The existing conversion path keeps its exact-date rule; later P13M0 policy integration must be verified before using the new policy for live valuations.
Each card: what / when / what you see / if it fails / taught in. The complete 65-line crontab (every configured run) is V0 §6; counts in COVERAGE.md Matrix 3.
SettlementRunner — DAILY · WEEKLY · MONTHLY · QUARTERLY
| When | 0 1 * * * · 0 2 * * 1 ·
0 3 1 * * · 0 3 1 */3 * |
| What | Fans out period settlement for the given frequency |
| You see | New/updated settlement periods and pipeline jobs; statements after finalisation |
| If it fails | Oban settlement queue retries; check period status
stays DRAFT/PRELIMINARY; re-run from Contract Settlement when meters are
ready |
| Taught in | V8 §2 · V8 §11 |
ChainReconciliationRunner
| When | 0 4 1 * * (1st, after monthly settle) |
| What | Cross-PPA chain check (volumes / trader margin) |
| You see | Chain recon results on trader/settlement views |
| If it fails | Queue retry; investigate broken chain links before forcing a re-settle |
| Taught in | V8 §10 |
LevyConservationRunner
| When | 10 4 1 * * (1st, after chain reconciliation) |
| What | Proves every in-force non-bypassable levy produced a settlement line on the prior month |
| You see | A failed Oban job when a period is missing a mandatory levy; billing preflight refuses the same month |
| If it fails | The job raises (unlike chain reconciliation, which
only logs). Inspect the named period and category, then re-settle or add
the missing NetworkRate |
| Taught in | V8 §4.2.1 · V8 §10 |
RunBackcast
| When | 0 4 4 * * (4th) |
| What | Settled strategy actuals — backcast prior month after recon |
| You see | Updated strategy snapshots / backcast runs on books |
| If it fails | strategy queue; confirm prior month settlement
closed |
| Taught in | V3 (settled actuals / scoring) |
RunNightly
| When | 30 2 * * * |
| What | Refresh every book’s strategy forecast; resolve BRP BESS market inputs first |
| You see | Fresh FORECAST runs, MARKET panel inputs ready for DAM |
| If it fails | Book stuck on stale run — check
brp/strategy queues |
| Taught in | V3 · V9 §1 · V9 §12 |
ProposeNominations
| When | 0 5 20 * * (20th) |
| What | Propose next month’s nominations for nominated memberships |
| You see | Nomination plans move toward PROPOSED on Nominations screens |
| If it fails | Manual propose from the book; check membership flags |
| Taught in | V3 §9 |
SubmitDamBids
| When | 0 3 * * *, while the actual D−1 gate is derived from
MarketRules |
| What | Builds and submits day-ahead bids for tomorrow’s delivery |
| You see | Bid Desk rows advance; a gate-miss is visible when the run is too late |
| If it fails | Check the brp queue and MarketRules before retrying; do
not submit a stale market day blindly |
| Taught in | V9 §4 |
FetchSchedule
| When | 0 12 * * * |
| What | Pulls the accepted DAM schedule after clearing |
| You see | Schedule values attached to the cleared bid/day |
| If it fails | Confirm the bid reached its gate and inspect the brp
queue; an empty schedule is evidence, not a zero schedule |
| Taught in | V9 §4 |
ComputeImbalance
| When | 30 6 * * * for D+1 |
| What | Prices yesterday’s actual-versus-scheduled deviations |
| You see | Imbalance hours and their calculated exposure |
| If it fails | Verify actual readings and the accepted schedule first, then retry
on the brp queue |
| Taught in | V9 §6 |
IdmMonitor
| When | 0 4,10,16,22 * * * (the five §10(1) Code gates) |
| What | Watches delivery-day drift and prepares intraday response |
| You see | IDM monitoring state and re-bid candidates |
| If it fails | Inspect the brp queue and current market clock; do not
assume a missed monitor is a DAM failure |
| Taught in | V9 §5 |
PollSo
| When | */15 * * * * |
| What | Ingests System Operator dispatch, curtailment, and outage instructions |
| You see | Active SO instruction cards and affected delivery windows |
| If it fails | Inspect the feed and brp queue; preserve the source
evidence before a re-bid |
| Taught in | V12 §3.2 |
ArchiveSoInstructions
| When | 5 0 * * * |
| What | Archives completed SO windows while retaining searchable history |
| You see | Active inboxes lose completed windows; date search still finds them |
| If it fails | The inbox may be noisy but dispatch history is not deleted; correct
the job on brp and retry |
| Taught in | V12 §3.2 |
RefreshPriceForecast
| When | 30 12 * * * |
| What | Refreshes the trailing-quantile SMP forecast after DAM information |
| You see | A later forecast version feeding the market/strategy view |
| If it fails | Check input freshness and the brp queue before treating
an old forecast as a market price |
| Taught in | V9 §9 |
ReconcileMonth
| When | 0 7 3 * * |
| What | Compares the internal expectation with the MO settlement file |
| You see | A reconciliation worklist and variances |
| If it fails | Confirm the MO file arrived, then investigate variance inputs rather than overwriting them |
| Taught in | V9 §8 |
MonthlyForecastRunner
| When | 0 4 5 * * (5th) |
| What | Fan-out one ForecastRunner per active IPP for the
target month |
| You see | New forecast versions on IPP Forecast screens |
| If it fails | forecast queue; confirm IPP has an ACTIVE physical
PPA |
| Taught in | V4 (monthly forecast) |
FetchWeatherCycle · FetchWeatherDrift
| When | 0 2 * * * (cycle) · 0 6,9,12,15 * * *
(drift) |
| What | One weather pull per generator site (72 h horizon, jittered) an hour
before DAM; four later nowcast issues on the same :forecast
queue |
| You see | Forecast / committed / nowcast bands on the IPP strategy dashboard; Bid Desk provenance names provider and model-run |
| If it fails | Site shows stale or no forecast — synthetic
shape, never a silent zero night; check the
forecast queue and the mock adapter |
| Taught in | V0 §11 · V3 |
CertificateRunner · Cadt.SyncRunner · Cadt.PublishRunner
| When | 45 4 1 * * (accrual) · 20 7 * * *
(observer) · 50 7 * * * (publisher) |
| What | Accrue RECs from FINAL settlement; CAD Trust observer sync; testnet publisher until the mandate |
| You see | New certificate batches on the Desk; observer rows on
/operator/cadt |
| If it fails | Accrual cannot be invoiced at 05:00 the same morning — a human must confirm first; CADT jobs move no money |
| Taught in | V19 · §9 |
Billing and insight (1st of month)
| When | 0 2 aggregates · 30 4 statements ·
0 5 issuer · 30 5 org billing ·
0 6 facts · 30 6 planner (all on the 1st) |
| What | Settlement documents, invoices, organisation billing, insight facts, opted-in planner |
| You see | New statements then invoices; insight facts only after invoices exist |
| If it fails | Do not re-run the issuer before statements exist; facts before invoices are empty by design |
| Taught in | V15 · V16 · V8 |
DamCadence · IdmCadence · DeliveryCadence · SettlementCadence · TrustInterest
| When | MO operating cadence in config/config.exs (gate close
08:00, unconstrained, constrained, prices, publish; five IDM auctions;
metering 00:15/00:30; balancing 15:00–15:30 D+1…D+7; weekday
statements/cover; 1 Sep calendar; 02:00 reassertion; 22:15 trust
interest) |
| What | The Market Operator’s own clock — not the BRP’s bid workers |
| You see | Cadence ledger at /mo/cadence; schedules and prices
after publish |
| If it fails | Inspect /mo/cadence before re-running a step;
publishing is a separate human action on Clear Day |
| Taught in | V11 · V0 §6 |
DisputeClock · ApplicationClock · MecBreachScan · RunStochastic · RunFleet
| When | 15 5 * * * · 30 5 * * * ·
5 0 * * * · 45 2 * * * ·
15 3 * * * |
| What | Bilateral dispute reminder then lapse; BRP application clock; prior-day generator MEC scan; site stochastic VaR after RunNightly; fleet aggregation after site results |
| You see | Dispute / application board already correct when the desk opens; site VaR before 03:00 DAM; the four-chapter fleet story and correlated VaR on IPP Sites → Fleet |
| If it fails | Check settlement / brp /
strategy queues; a missed VaR is not a reason to skip the
bid |
| Taught in | V8 · V9 · V3 §22 · V4 §2 |
3.4 Enqueue-driven workers and setup helper
These five are Oban workers but do not have an independent cron line.
Their “when” is the event that enqueues them.
BackfillMeterHistory is included because operators
encounter it during S05, but it is a setup helper—not an
Oban.Worker.
PipelineRunner
| When | SettlementRunner fans it out for an allocation set |
| What | Resolves, matches, prices, adjusts, and writes one settlement pipeline |
| You see | Period progress and updated DRAFT/PRELIMINARY result data |
| If it fails | Inspect meter completeness and settlement; rerun the
controlled settlement path, never edit a FINAL period |
| Taught in | V8 §2 · V8 §11 |
StatementRunner
| When | Settlement finalisation enqueues statement production |
| What | Produces party-facing statements from the final settlement result |
| You see | Statement records/versioned views after the period is FINAL |
| If it fails | Verify finalisation and the settlement queue; a missing
statement is not evidence that money was not settled |
| Taught in | V8 §9 |
ForecastRunner
| When | MonthlyForecastRunner fans out, or an operator explicitly enqueues it |
| What | Creates one IPP monthly forecast version |
| You see | A fresh forecast on the IPP forecast screen |
| If it fails | Check forecast, active physical PPA eligibility, and
source period before retrying |
| Taught in | V4 §8 |
RunAllocation
| When | Manual or legacy strategy shell requests it; nightly work normally uses RunNightly |
| What | Executes the requested allocation path for a strategy run |
| You see | A strategy run record and its allocation result |
| If it fails | Check the strategy queue and inputs; avoid attributing
it to the S15 meter loop |
| Taught in | V3 §5 |
IngestReadingWorker
| When | Each S15 simulator tick enqueues it |
| What | Persists the reading produced by the live-meter simulator |
| You see | New interval/device readings after the tick |
| If it fails | Check default/the worker’s queue and stop live before
mutating setup; do not use S05 history as a repair |
| Taught in | §6 · V2 §7 |
BackfillMeterHistory
| When | S05 during mix wb.setup |
| What | Builds deterministic historical meter rows and deliberate missing-read fixtures |
| You see | Roughly 12 months of historical intervals, including gallery gaps |
| If it fails | Resume the same setup manifest and inspect setup; it is
not a cron retry and not an Oban worker |
| Taught in | §4 · V2 §7 |
3.5 AshOban triggers (4)
| Trigger | Cadence | What | Taught in |
|---|---|---|---|
PhysicalPpa :activate |
daily 0 0 * * * |
SIGNED → ACTIVE when start_date ≤ today |
V7 |
PhysicalPpa :suspend_unsigned |
daily 0 1 * * * |
AGREED past 10-day sign window → SUSPENDED | V7 §5.1 |
VirtualPpa :suspend_unsigned |
daily 0 1 * * * |
Same for virtual AGREED deals | V7 §5.1 |
Proposal :expire_stale |
hourly | PENDING + past expires_at → EXPIRED |
V7 · §7 |
3.6 Long-running sim surfaces (3)
| Process | Kind | Cadence | Taught in |
|---|---|---|---|
WB.Sim.LiveMeter.Simulator |
GenServer (started by
mix wb.setup --profile live, not in app children) |
Virtual +20 min / tick; wall default 20 min | §6 |
WB.Sim.Driver |
Callable module (Task under
Sim.TaskSupervisor from Lab) |
Day-by-day sim-clock month in seconds | §5 |
WB.Sim.Market.DamSubmitter |
GenServer (app child when
:simulated_market_submitter is true — dev
only) |
Boot tick, then daily 03:00 SAST; no-op if the S16 overlay is absent | §4.7 |
Setup and test pin two further transports so those runs never
reach a network: :sapp_client →
WB.Sim.SappSim and :fx_provider →
WB.Fx.Provider.Mock. A missing published rate
blocks rather than degrades — an operator seeing
blocked Regional-desk actions should look at the FX register first. Live
fetching requires explicit configuration and source policy; live
valuation remains gated on PD-F8-1 and the later conversion integration.
Taught in V18.
3.7 Mix tasks (5)
| Task | What | Taught in |
|---|---|---|
mix wb.setup |
Consolidated demo/acceptance control plane
(--include-market appends S16) |
§4 |
mix setup.market |
Foundation plus the fifty-participant SAWEM overlay (profile
market) |
§4.7 |
mix setup.market_month |
A whole Billing Period traded through that overlay (S17);
--rebuild resets first |
§4.8 |
mix wb.forecast_backfill |
Historical IPP monthly forecasts --from/--to |
V4 |
mix wb.replay_hierarchy_diff |
Re-settle one IPP month through hierarchy; print matched/surplus report | V4 · V8 §5 |
Queues to watch:
default,proposals,setup,contracts,settlement,strategy,brp,forecast. FAQ for stuck jobs: §7.
3.9 The Market Operator’s Clear Day panel
Clearing a Trading Day is not a background job. It is four steps on the MO console, in order, with a human between the third and the fourth.
| Step | What it does |
|---|---|
| 1. Run unconstrained | Solves the market’s own answer: cheapest way to meet the submitted demand, ignoring the network. This is the run that sets the price. |
| 2. Apply SO constraints | Attaches the System Operator’s security limits — corridor capacities, zonal export caps. |
| 3. Run constrained | Solves again with those limits. This is the run the system is dispatched from. |
| 4. Publish | Makes both schedules and all five prices the thing every participant settles against. |
Publishing is separate from solving, and that is the point of
the panel. The obligation is to publish by 14:00 on the day
before delivery. An operator who has just re-run a day needs to look at
it before it becomes everybody’s settlement basis — so Publish
is its own button, behind its own confirmation, and a solved run sits in
SOLVED until somebody presses it.
Solving again does not edit a published run. It creates a new one, and publishing that one supersedes the old one, which stays readable. That is what makes “why did my statement change” answerable months later.
Three things on the panel are worth reading rather than skimming:
- The marginal unit per period — who set the price. If it is not who you expected, the reason is usually one of the four inflexibility grounds (V9 §4.13).
- The
[D-08]badge — the period’s price under the other reading of whether a curtailed load may set the price. It appears only where the two readings differ, and the difference can be four figures. - The network model notice — whether the constrained run bound on a published System Operator model, on labelled assumptions, or on nothing at all. Under the provisional zone register it is the third, and the constrained schedule is then identical to the unconstrained one. The notice is what stops that being read as “the network was checked and found clear”.
Requires the HiGHS solver on the host
(brew install highs, or your platform’s package). Unit
commitment with minimum run and down times cannot be solved correctly by
any greedy method, so an absent binary is a deployment fault rather than
a reason to fall back — the panel says so instead of returning a
schedule that quietly ignores commitment.
The public price board at /prices needs
no login at all: the market price set is published information, and a
register only participants can read is not published.
Hydrology fetch runs Tuesdays at 01:30 on the forecast queue. The generation bridge runs monthly at 04:50 on settlement, after certificate accrual, proposing fuel activity from FINAL generation. Neither job submits a tax declaration or arms a live provider.
Trading-control workers
| Worker | Schedule (SAST) | Evidence |
|---|---|---|
WB.Brp.Workers.ProposeEscalation |
09:30 daily; checks the effective gate-minus-30-minute window | One persisted escalation per registration and trading date; telemetry, critical log and refresh notification; never submits |
WB.Org.Workers.ExpireApprovals |
Every 15 minutes | Pending requests past their expiry become EXPIRED; retries are idempotent |
WB.Brp.Workers.SelfAssessmentSnapshot |
1 January at 06:00 | Previous calendar year’s organisation-scoped pack, with a deterministic content hash |
Forecast evidence workers
The scheduler uses UTC. ScoreSkill runs Monday 04:30 UTC
(06:30 SAST), creating one job per weather site and prior Monday week.
Re-running identical inputs reuses the evidence version.
BuildSoPack runs daily 07:30 UTC (09:30 SAST) for the next
seven delivery days; BuildSoRolling runs hourly at :40 for
six hours from the next full hour. Both retain category and data
refusals as records. These workers build evidence and never push it to a
live operator. The pack transport resolves to Mock unless explicitly
configured otherwise.
Data fabric workers
FreshnessSweep runs every fifteen minutes;
AttemptRetention at 01:10 UTC preserves ninety days and the
latest attempt. DeadLetterSweep runs at 00:20 UTC.
DeliverWebhook is event-driven on the events
queue (concurrency five) with eight attempts per schedule. These jobs do
not alter the monthly settlement-chain order.
DispatchPending runs each minute to recover committed
outbox rows when an enclosing transaction did not forward its Ash
notifications. A rolled-back source transaction leaves no delivery.
Plant telemetry workers
| UTC schedule / trigger | Worker | Operational result |
|---|---|---|
| Every five minutes | Telemetry.Workers.IngestMirrorDrop |
Process the configured plant-owned historian inbox; archive each file and its result |
| Minutes 1, 6, …, 56 | Telemetry.Workers.FreshnessSweep |
Record stale/recovered transitions and update the feed registry |
| Minutes 2, 17, 32, 47 | Telemetry.Workers.IntradayMecScan |
Provisional warnings from good device power; no billing |
| Instruction end + 15 minutes / curtailment-off event | Telemetry.Workers.BuildCurtailmentTrace |
Freeze samples and method inputs; retry missing meter coverage hourly for up to 48 hours |
These four workers share telemetry concurrency five.
Vendor pull workers are not installed: P11M2 remains trigger-gated.
Compression applies to raw telemetry older than 30 days on a capable
Timescale build; raw retention is 400 days. The hourly aggregate is
retained. Monitor the feed registry and Oban for refusals and
retries.
4. Demo & acceptance data
How the seeded world is built. This chapter states the procedures,
profiles and safety rails in plain language. Copy-paste commands live on
the control-plane tasks (mix wb.setup,
mix setup.demo, mix setup.market).
4.1 Safety rails
Before any mutating setup command:
- Stop Phoenix, Oban peers, IEx, Scenario Lab drivers and the
live-meter runtime. Call
WB.Setup.stop_live()if a live tick loop is still up. - Use a local, disposable database only. The control
plane refuses
MIX_ENV=prod. - Every data profile requires
--reset— an explicit acknowledgement that teardown will destroy application data in scope. - Prefer an explicit
--as-of YYYY-MM-DDand--seed(default 42) so demos reproduce. Budget ~2 GB free and ten+ uninterrupted minutes for a full build.
4.2 Profiles
| Profile | Stages | Duration |
|---|---|---|
foundation |
S00–S03 | < 1 min |
full_demo |
S00–S14 + S18 | 3–6 min; ~209 s verified |
acceptance |
S00–S14 | environment-dependent |
market |
S00–S03 + S16 | ~1–2 min |
market_month |
S00–S03 + S16 + S17 | ~10–25 min |
lab |
scoped S12 | 2–4 min |
stress |
scoped S03 | environment-dependent |
live |
S15 (guarded) | continuous |
Pass --include-market on foundation,
full_demo or acceptance to append S16 after
that profile’s last stage. The dedicated market profile
already includes S16. See §4.7.
S18 · Regional desk is part of
full_demo: a sponsored SAPP participation profile, a small
FPM + DAM book, a cross-border path, and a shadowed month with a
classified variance, all against SappSim and the fixture
rate sets. The sponsor is labelled illustrative. Setup pins
:sapp_client and :fx_provider to in-process
mocks — those runs never reach a network. The trader-seat volume is V18.
market_month is the longest deterministic profile there
is: it runs the fifty participants through every Trading Day of
a month — day-ahead clearing, prices, publication, metering,
allocation, balancing, statements — and then bills the month. See §4.8.
4.3 Stage graph S00–S17
| Stage | Seals |
|---|---|
| S00 | Preflight, lock, teardown, zero residue, manifest. Also clears local
written-agreement PDFs under priv/agreements/generated/
(dev/test); production S3 objects are out of teardown scope |
| S01 | Localities, orgs, users, Eskom/MO/SO seats |
| S02 | Tariffs, losses, UoS, municipal wheeling ref |
| S03 | Sites, pools, devices, accreditation variants |
| S04 | Proposal / contract lifecycle and executed written
PPA PDFs for every dual-signed physical contract
(GenerateAgreement → ExecutedAgreement on
local disk in Dev/Test) |
| S05 | ~12-mo meter history + missing-read gallery |
| S06–S07 | Strategy books, runs, noms, BESS, sims |
| S08 | BRP registrations, DAM/IDM, SO, clock replay |
| S09–S11 | Settlement matrix (re-seals agreement PDFs for any new dual-signed physical PPAs), statements, chain recon / disputes |
| S12–S13 | Scenario Lab scripts; UI status gallery |
| S14 | Exhaustive verify → COMPLETED |
| S15 | Live ticks (needs matching S14 as_of/seed) |
| S16 | Fifty simulated SAWEM participants (15 IPP / 15 off-taker / 20
trader), MO+SO registration, initial DAM. Overlay — not part of default
full_demo |
| S17 | A whole Billing Period traded through the S16 overlay: every Trading Day cleared, priced, published, metered, allocated, balanced twice and statemented, then the month billed and paid (§4.8) |
Written agreement PDFs in setup
Dual-signed physical PPAs (SIGNED, ACTIVE, TERMINATED in the gallery) get a long-form PDF during setup — the same pipeline as live signing (V5 §6.3).
| Step | What happens |
|---|---|
| S00 teardown | Truncates executed_agreements with the rest of
application data and rm -rfs
priv/agreements/generated/ so orphan PDFs cannot survive a
rebuild |
| S04 | After the proposal/contract gallery is built, setup drains the
agreements Oban queue and asserts every dual-signed
physical PPA has a GENERATED PDF (local file://… URI in
Dev/Test) |
| S09 | Settlement’s contract matrix may create more dual-signed physicals; the same seal runs again so those PDFs exist before settlement stories |
| Rebuild | Procedure B
(mix wb.setup --profile full_demo --reset …) therefore
always recreates the PDF set from the signed snapshots |
Download path after setup: Contracts → open a SIGNED/ACTIVE physical
→ executed-agreement download (/org/agreements/:id). Thread
example: PPPA-202607-138209764 (IPP2 Atlantis Wind →
OffTaker C) once that gallery ref is present.
4.4 The procedures
A — recreate the database, then set up once. Use
this only when you intend to discard migrations’ application data and
setup audit history. Do not use mix ecto.reset followed by
full_demo: seeds.exs competes with the setup
control plane.
mix ecto.drop
mix ecto.create
mix ecto.migrate
mix wb.setup --profile full_demo --reset --seed 42B — teardown and rebuild while retaining the database
shell. This keeps extensions, migrations, and
setup_audit, then reconstructs the deterministic world.
mix wb.setup --profile full_demo --reset --seed 42C — tear down once, then advance deliberately. Copy the manifest ID that S00 prints and resume that exact manifest at each next stage; this lets you inspect the boundary where a pack changes the data.
mix wb.setup --profile full_demo --reset --through S00 --seed 42
mix wb.setup --resume MANIFEST_ID --through S01
mix wb.setup --resume MANIFEST_ID --through S14D — roll an already-sealed demo forward to today.
When a prior full_demo was sealed on an older business
clock (for example as_of 2026-07-18) and you want Strategy
Overview, Scenario Lab and market artifacts to sit on
today’s UTC date without inventing a new seed, use:
mix wb.setup.roll_forward
# equivalent:
mix setup.roll_forwardOptions: --as-of YYYY-MM-DD (default: today UTC),
--seed N (default: inherit the prior completed run’s seed,
else 42), --profile full_demo|acceptance|lab,
--dry-run, --force (rebuild even when
as_of is already current). This is still destructive — it
stops the live-meter runtime and runs the same reset-first rebuild as
Procedure B with the resolved clock/seed. Stop Phoenix, Oban peers and
Scenario Lab drivers first. The Demo Dashboard’s Update demo to
today runs this same procedure in the background (§8.2).
Expected success: manifest status: "COMPLETED",
current_stage: "S14", teardown_complete: true,
no failure.
4.5 Credentials pointer
Every demo user password is
supersecret. Seats include
eskom@webill.net, mo@webill.net,
so@webill.net, org seats *_@webill.net,
municipality = normalised name + @webill.net.
4.6 Recovery playbook
If a setup stops, leave the failed world intact long enough to
identify the manifest and failed stage. Read recent
setup_audit records, then correlate the manifest with jobs
on the setup queue. Resume the same manifest
ID after correcting the cause; starting another destructive
profile over a half-built world destroys the useful evidence and can
leave mixed fixtures.
| Symptom | Safe response | Do not do |
|---|---|---|
| A stage reports failure | inspect stage output, fix the pack/input, resume its manifest | start a second --reset |
| Setup queue is delayed | inspect oban_jobs and its error/retry state |
hand-mark the stage complete |
| Credentials do not work | correct S01 and rebuild | patch password hashes |
| Live process is running | call WB.Setup.stop_live() before a mutating run |
overlap S15 and reset |
| Full demo is too broad | rebuild with foundation or lab for the
stated goal |
delete selected fixture rows |
Careful.
--resetis an acknowledgement that the profile tears down data in scope. The guard againstMIX_ENV=prod, the live-overlap refusal, explicit seed/as-of values, and the manifest audit are safety rails—not inconveniences to bypass.
Lab profile for Scenario Lab drills: §5. Live profile for S15: §6. Simulated market overlay (S16): §4.7. A traded month (S17): §4.8.
4.7 Simulated SAWEM market
The ordinary demo cast already has a handful of BRPs (S03 parties,
S08 BRP-T1-DEMO and the status gallery). The
simulated market is an optional overlay of
fifty extra participants that sit beside that cast,
register with both the Market Operator and the System Operator, and
submit a day-ahead series every day.
It is not part of default full_demo.
S14 still seals the ordinary world first; S16 appends afterwards so
existing counts stay valid.
Who is in the overlay
| Kind | Count | Technologies / categories | Org / BRP prefix |
|---|---|---|---|
| IPP | 15 | Solar (4), Wind (4), Hydro (3), Coal (2), Gas (2) — 1 300 MW of export capacity | SIM IPP … / BRP-SIM-IPP-… |
| Large off-taker | 15 | Mines (5), factories (5), malls (5) — 497 MW average draw, 90 MW down to 8 MW | SIM Mine/Factory/Mall … /
BRP-SIM-OT-… |
| Trader | 20 | One book per participant, zones Z1–Z9 cycling | SIM Trader … / BRP-SIM-TRD-… |
The off-takers carry industrial-scale load on purpose. A cast averaging a couple of megawatts each against 1 300 MW of generation would clear at the price floor in every hour of every month and settle to almost nothing; at 497 MW it peaks around 620 MW against roughly 840 MW of available supply on a winter evening, so the market has a marginal unit in it and the System Marginal Price moves.
Every organisation name starts with SIM, every BRP code
with BRP-SIM-, and every meter is tagged
SIM-MARKET. Sign-in emails are
sim_ipp_solar_01@webill.net,
sim_ot_mine_01@webill.net,
sim_trader_01@webill.net, and so on. The password is still
supersecret. Overlay generator meters
(including coal MTR-NERSA-SIM-GEN-12 / 13 and
gas 14 / 15) have no wheeling interval history
until you Generate Reads on the site — that button uses
the same technology shapes as the rest of the platform (coal baseload,
gas morning and evening peak). SAWEM market intervals from S17 are a
different table and do not fill that explorer.
Each participant is:
- an organisation + user (S01-style credentials);
- a site or trader pool (generator / off-taker / pool);
- an ACTIVE BRP registration with the Market Operator
(
GENERATOR/CONSUMER/TRADER, Annexure 1 criterion, MPID assigned); - a Trading Unit (and Point of Delivery where the site’s GPS falls inside the provisional network model) with the System Operator via the same onboarding path the wizards use. A trader registers a Trading Unit with no Point of Delivery of its own: §12.3(1) makes it the counterparty at the IPP’s and the off-taker’s meters, and inventing a meter for it would double-count energy that already has two owners;
- a 168-period §9.3 DAM series for tomorrow, shaped by technology or load (solar is daylight-weighted; coal is baseload; malls peak in business hours).
How to seed it
Dedicated rebuild (foundation + overlay,
reset-first) — same shape as mix setup.demo:
mix setup.market
# equivalent:
mix wb.setup --profile market --reset --seed 42Add the overlay to an existing profile without changing that profile’s own stages:
mix setup --include-market
mix setup.demo --include-market
mix wb.setup --profile full_demo --reset --include-market --seed 42mix setup --include-market sets
WB_SETUP_INCLUDE_MARKET=1 so
priv/repo/seeds.exs appends S16 after foundation.
From the Demo Dashboard (/demo, §8): the Simulated SAWEM
market card shows whether the overlay is present.
Include simulated market seeds S16 in the background
without teardown — the ordinary demo world stays. Use
this when foundation or full_demo is already sealed and you
only want the fifty participants.
WB.Setup.seed_simulated_market/1 is the same overlay
write the button calls.
Daily DAM submissions
WB.Sim.Market.DamSubmitter is a GenServer started with
the application when :simulated_market_submitter is true
(dev only; off in test and prod). On boot it submits
tomorrow’s series if the overlay is present and any unit is still
missing one, then sleeps until the next 03:00 SAST —
the same window as WB.Brp.Workers.SubmitDamBids. A missing
overlay is a no-op: foundation-only boots stay quiet until
mix setup.market or Include simulated
market lands the cast.
Capacity follows a technology (or load) shape so the fifty participants do not all declare a flat MCR. A unit that already has an operative series for the Trading Day is skipped.
The Demo Dashboard’s market card reports participant counts, the next Trading Day, whether that day’s DAM is in, and whether the submitter is alive — and its daily checks can backfill Trading Days the 03:00 window slept through (§8.4).
S16 itself posts no meter readings — it declares
tomorrow, it does not measure yesterday. Metering arrives with S17,
which submits 20-minute coverage as a simulated Network Operator for
every metered BRP-SIM-… Point of Delivery; the design it
follows is docs/references/market/spec/no-meter-readings.md.
Do not point the wheeling live-meter runtime at these participants —
that runtime writes ts_readings for active PPA devices and
is a parallel world.
S16 also seeds two months of synthetic market
invoices so /org/market-accounts is not empty before
anything has traded. Where S17 then trades one of those months for real,
it cancels the placeholders and raises its own documents — see §4.8.
Data.
market· seed 42 · stage S16 · fiftyBRP-SIM-…parties beside the ordinary S03/S08 cast. Daily writer: §3.6.
4.8 A month of market trading
S16 gives you a market that can be looked at: fifty registered participants and tomorrow’s day-ahead series. S17 gives you a market that has run — every Trading Day of a chosen Billing Period taken through the Market Operator’s own chain, and the month closed with statements, invoices, payments and a billing board.
Everything on this page is written by WB.Sawem.Cycles —
the same functions the scheduled jobs call, given an explicit clock.
There is no second implementation of the market in the seed. A step that
would miss in production misses here, on the cadence ledger, with its
clause.
What a Trading Day of the seeded month does
| Clock (SAST) | Step | Callable |
|---|---|---|
| D−1 09:00 | Participants submit their 168-period §9.3 series, with production or consumption curves | Submission.Intake.put/2 |
| D−1 10:00 | The gate closes | close_dam_gate/2 |
| D−1 10:05 | Whoever missed it is rolled over under §9.3(2) | generate_rolled_over_submissions/2 |
| D−1 11:00 | Unconstrained clear | clear_unconstrained/2 |
| D−1 11:30 | Constrained clear | clear_constrained/2 |
| D−1 12:00 | Prices and the §9.8–9.9 settlement lines | calculate_prices/2 |
| D−1 14:00 | §9.10 schedule reports published | publish_schedule_reports/2 |
| D 11:00 | The Intra-Day auction, iteration 3 | clear_idm/3 |
| D+1 06:00 | The Network Operator’s 20-minute metering batch | ingest_metering/2 |
| D+1 06:30 | Provisional allocation | allocate_metering/2 |
| D+1 08:00 | Preliminary balancing | run_balancing/3 |
| D+1 17:00 | Indicative settlement statements | issue_indicative_statements/2 |
| D+7 06:30 | Verified allocation | allocate_metering/2 |
| D+7 08:00 | Official balancing | run_balancing/3 |
Provisional then verified is not busywork. §12.2(2) gives seven days to verify metering and §13.6(1) keeps balancing preliminary until D+7, so a seed that only ran the official pass would leave the desk nothing to compare against.
What month end does
| Clock | Step |
|---|---|
| BP+5WD 12:00 | Initial statements, one per party, one line per settlement kind per Settlement Day |
| the same instant | Invoices and self-billing invoices, with their §15.3.4(2) clocks a day apart |
| BP+5WD | The monthly §15.1(1)(d) Market Operator Charge, into the ring-fenced MO Charge Account |
| invoice due date | Participants pay by EFT — three business days |
| self-billing due date | The Market Operator pays out — four business days |
| BP+6WD | Credit cover reassessed for every party |
| BP+5WD | Mo.Settlement.Month.run/3 persisted for the billing
board |
| BP+1M | The timetabled settlement rerun |
Collection comes before distribution because the Code puts it there (§15.3.4(2)(d),(e)), which is what lets a Shortfall reach the creditors in the same cycle rather than the next one.
Where the Trading Clearing Account has collected less than the month owes — which is what a defaulter does to it — every creditor is paid its share, not the first few in full and the rest nothing. §15.3.4(11) allocates a bad debt across the creditors in proportion to what each was owed, and the cash side follows.
Money in the seeded month
WB.Brp.Registration opens every party’s own bank account
with the standard R3 million float when the
registration is created. A month’s energy bill for the largest off-taker
in the cast is around R43 million, so before the first
Trading Day the seed tops each account up to a R250
million working balance and posts R25 million
of it as credit cover. Both are labelled as notional funds from the
participant’s own business, never as income from the market — the same
honesty rule the opening balance itself follows.
Without the top-up every participant is an insufficient-funds refusal, and a month in which nobody could pay teaches nothing about §15.3.4: the ladder is only interesting when the participants who can pay do.
The monthly §15.1(1)(d) Market Operator Charge is R18 500 per party and lands in the ring-fenced MO Charge Account, which §15.3.4(6) forbids from ever funding a shortfall or a cascade.
The days that are not ordinary
A month of thirty-one identical days proves the pipe and teaches nothing, so the seed places each interesting failure deliberately:
| Day of month | What happens, and where you see it |
|---|---|
| 4, 18 | Three units miss the 10:00 gate; §9.3(2) rolls their previous series
forward. /mo/cadence |
| 7, 21 | A zonal export limit is declared, so the constrained run diverges from the unconstrained one and §9.8.2 pays for it. Needs HiGHS installed; without it the day runs as an ordinary one |
| 9, 23 | The System Operator instructs four units over three evening hours;
§13.4 / §13.9 legs follow. /mo/balancing |
| 11 | Metering arrives :ESTIMATED and never verifies, so the
official balancing run refuses with its clause rather
than settling on estimates. /mo/metering,
/mo/balancing |
| every 3rd | An Intra-Day auction clears iteration 3 against lodged
redeclarations. /mo/bidding/idm |
Month end adds two more: the largest debtor pays 60 % of its invoice and cures its Credit Call inside the window; the smallest pays nothing and does not, so its Shortfall becomes Unsecured Bad Debt and the §15.3.4(11) cascade allocates it across the creditors. One month, both branches of the ladder.
How to seed it
From the Demo Dashboard (/demo, §8), with the overlay present: the
Market trading card offers the last six complete
Billing Periods. Pick one and press Populate trading
month — or press Trade the last 3 months and
let it take the incomplete ones oldest-first. It runs in the background,
and the progress bars are read from the rows the seed has written, not
from a counter in the page, so they survive a reload and a second
browser.
From the command line:
# Add a month to an overlay that is already seeded — no teardown.
mix setup.market_month --month 2026-07
# Rebuild foundation + the fifty participants + the month, reset-first.
mix setup.market_month --month 2026-07 --rebuild
# A three-day smoke run.
mix setup.market_month --month 2026-07 --days 3--month defaults to the last complete calendar month.
WB.Setup.seed_trading_month/1 is the same write the button
calls.
Roughly what it writes
For a 31-day month and the fifty-party cast:
| Artefact | Rows |
|---|---|
| §9.3 submissions, 168 periods each | 50 × 31 |
| Published clearing runs, with prices and settlement lines | 31 |
| Native 20-minute meter readings | 24 metered points × 72 × 31 |
| Hourly rollups and Actual Energy, at two vintages | 24 × 24 × 31 × 2 |
| Balancing runs, preliminary and official | 62 |
| Indicative settlement statements | 50 × 31 |
| Initial statements, invoices and self-billing invoices | 50 + 50 |
| Cadence rows | ~14 × 31 |
Ten to twenty-five minutes on a laptop is normal.
Six meters that are not read, and why
Thirty of the fifty participants own a physical Point of Delivery:
fifteen IPP meters and fifteen off-taker meters, each paired with a
trader as counterparty so §12.3(1)’s two-BRP rule is satisfied. Six of
those thirty sit at locations the provisional network model
PROV-2026-07 does not cover, so they have no Network Zone —
and §3.3(4)(a) makes jurisdiction a property of the zone.
The seed does not invent a zone for them. It does
not submit readings it knows would be refused, and it does not give the
affected units an Actual Energy of zero either, because a unit settled
at exactly its schedule reads as a clean day rather than as one that was
never measured. Those units appear on the Market Operator’s own
unplaced-sites board (/mo/zones/unplaced) and in the run’s
unplaced_points. The other twenty-four are metered,
allocated and balanced.
The five traders with no wheeling book this month
(BRP-SIM-TRD-16 … 20) are in the same position
for the same reason: no meter, no balancing position — and still a full
month of declarations, cleared schedules, settlement lines, statements,
documents and accounts.
Running it twice
Idempotent, and resumable by construction. A Trading Day that has already published a clearing run, an official balancing run and an indicative statement for every party is recovered from the record rather than re-traded: the month gets its lines back from the published run and the balancing legs, and nothing is written twice. An interrupted month is finished by running the same command again.
§15.3.4 issues a statement once, so a repair run does not re-issue one. A revision is §15.3.5’s instrument, not a second issue of the same figures.
Where to look afterwards
| Screen | What the month put there |
|---|---|
/mo/bidding?date=… |
The day’s chalkboard: submissions, SMP curve, energy account netting to R0.00 |
/mo/bidding/month?month=2026-07 |
The billing board: statements, documents, the cascade, the cash position |
/mo/cadence?date=… |
Every callable that ran, missed or refused, with its clause |
/mo/metering?date=… |
Allocation status, vintages, the exception inbox |
/mo/balancing?date=…&stage=OFFICIAL |
Imbalance volumes, both stacks, both prices |
/mo/accounts |
The three §15.2(2) market accounts, designations, movements, credit cover |
/mo/billing |
Invoices and self-billing invoices with their PDFs |
/org/market-accounts |
The same month from a participant’s own seat |
Data.
market_month· seed 42 · stages S00–S03 + S16 + S17 · the month named by--month, default the last complete one. Verified bytest/wb/setup_acceptance/trading_month_test.exs(three days, gated onWB_SETUP_ACCEPTANCE=1).
5. Scenario Lab — pointer to V12
The Scenario Lab (/org/sim, development and demo builds
only) is taught in V14 ·
Scenario Lab.
That volume covers purpose, the three seeded recipes
(AUG-BASELINE, CODEX-S12-WARM,
CODEX-S12-ISOLATION), every editor panel, Runs, Compare,
the day log, Teardown, and the worked clone-and-run.
This chapter stays so older #5-scenario-lab links still
land, and so the operations catalog can point at a neighbour instead of
re-teaching the kitchen.
| If you need… | Go to |
|---|---|
| What the Lab is and how to use it | V14 §1 |
| Each seeded scenario | V14 §6 · §7 · §8 |
| Panels, actions, lists, reports | V14 §9 · §10 |
| Live meter ticks after seal | §6 |
| Demo rebuild / roll-forward | §4 |
| Strategy ribbon reading | V3 §17 |
Gating (unchanged). The route compiles only when
Application.compile_env(:wb_wheeling, :sim_enabled)is true (dev/test, off in production). At runtimeWB.Sim.sim_enabled?/0must also pass or you are redirected home.
Revised read
S12 also runs a Revised read demonstration against
its participant meters: one FINAL interval and one DRAFT interval
receive a substituted revision. The FINAL period keeps its settlement
and gains a locked-period finding; the DRAFT period queues a re-cut.
Original readings remain available. Repeating the script skips an
already revised demonstration interval. If either state has no eligible
interval, the S12 result explicitly reports
no_eligible_interval for that state.
6. Live simulation mode
S15 turns on a continuous live-meter writer after a sealed S14 world. It is not another deterministic rebuild of history.
6.1 S05 history vs S15 live
| S05 (seeded history) | S15 (live) | |
|---|---|---|
| When | During full_demo / acceptance setup |
After S14, via --profile live |
| What | Deterministic interval rows (~12 months) | Ongoing 20-minute ticks |
| Reset | Part of --reset teardown |
No --reset on the live profile |
| Writer | Setup pack / BackfillMeterHistory |
WB.Sim.LiveMeter.Simulator GenServer |
| Clock | sealed historical interval series | wall cadence with +20-minute virtual advance |
| Purpose | reproducible examples and acceptance evidence | observe ongoing ingestion after the seal |
| Safe stop | teardown rebuilds it | WB.Setup.stop_live() before any mutation |
Settlement and strategy treat live ticks like any other meter data once ingested — late cumulative reads still preserve energy (V2 §7).
S05 is therefore not a slow version of S15. S05 creates a fixed, inspectable past as part of a profile; S15 writes a moving present after that past has been sealed. Do not attempt to repair live drift by rerunning S05 over it.
6.2 Start live only from a matching seal
- Complete a sealed S14 with known
--as-ofand--seed. - Stop conflicting processes (
WB.Setup.stop_live()if needed). - Start live with the matching
--as-ofand--seed. The live profile is continuous and accepts no--reset.
mix wb.setup --profile live --seed 42 --as-of YYYY-MM-DD- The Simulator starts under
WB.Sim.LiveMeter.Supervisor(not in the default Application children). Each tick enqueuesIngestReadingWorker(§3).
The Demo Dashboard’s Catch up metering now (§8.4) runs this same profile against the sealed run’s own clock and seed — it is a button on the procedure above, not a different path. Update demo to today is the clock rebuild (§4.4), not this live-meter start.
Default wall cadence: 20 minutes per tick (virtual clock advances +20 minutes). Catch-up can run faster when behind.
Follow along. Seat: an organisation user after a sealed S14 world. Path: open a device or interval explorer, then observe after one live tick. You should see: a newly ingested reading rather than a rebuilt history.
6.3 Worked observation — one tick becomes a reading
Data. full_demo · seed 42 · S14 sealed, then S15 live · illustrative 20-minute tick.
| Input | Value |
|---|---|
| Starting virtual time | 10:00 SAST |
| Tick cadence | 20 wall minutes |
| Next virtual time | 10:20 SAST |
| Writer | WB.Sim.LiveMeter.Simulator |
| Ingestion job | IngestReadingWorker |
- Start S15 against the matching sealed profile. The GenServer creates the next simulated meter observation at the tick.
- At one cadence, the virtual clock moves R1:
10:00 + 20 minutes = 10:20 SAST; it enqueuesIngestReadingWorker. - The worker persists the reading. Open the device/interval explorer and verify the new timestamp rather than expecting a modified S05 record.
- A daily settlement run can subsequently include eligible fresh intervals. It will not overwrite a FINAL period merely because another reading arrived.
What you see on screen is a later interval row after ingestion. This is the pattern to expect on your own page; the simulator itself is not a settlement approval action.
6.4 What you watch
- New readings appear on device / interval explorers.
- Settlement DAILY runner (§3) can pick up fresh intervals on its next pass.
- Lab Driver runs are separate — do not confuse an accelerated scenario month with S15 wall-clock ticks (§5).
6.5 Stop before any destructive work
Call WB.Setup.stop_live() before teardown, second setup,
or leaving the machine. Setup refuses to overlap live
ticks with a mutating profile run — that refusal is intentional (§7).
Credentials / safety still apply (§4). Never point live at a shared or production database.
Where this is taught. Meter and interval interpretation is taught in V2 §7. The ingestion worker appears in §3; controlled settlement status is taught in V8 §11.
6.6 A safe live-session checklist
Treat a live session as an observation window. Start from one sealed S14 manifest, keep its as-of date and seed with your notes, and tell other operators that S15 is writing intervals. When you are finished, stop the writer before opening a Scenario Lab drill or rebuilding fixtures.
| Before start | During observation | Before teardown |
|---|---|---|
| matching S14, seed, as-of | new interval timestamps and ingestion jobs | WB.Setup.stop_live() completed |
| local disposable database | no concurrent mutating setup | no active ticks remain |
| no stale simulator process | distinguish S15 from Lab Driver | resume/rebuild only after stop |
If an interval does not appear, inspect
IngestReadingWorker and the live supervisor before manually
adding readings. Manual repair hides the very ingestion behavior that
S15 was meant to demonstrate.
Plant telemetry companion
S15 starts LiveTelemetry.Simulator beside the meter
simulator. It advances a virtual five-minute clock and submits
deterministic Mock device payloads through the production normaliser and
plausibility engine. Ten-minute feeds emit only on their own boundaries.
It starts only with the guarded live profile; an ordinary application
boot does not start it. The mock badge remains visible, and
no vendor HTTP call is made.
7. Troubleshooting & FAQ
When a screen looks wrong, identify whether the source is configuration, readings, lifecycle, or a background worker before changing data. This chapter gives the first safe check and points to the section that owns the mechanism. If this summary conflicts with the owning volume, follow the owner.
Settlements and statements
Why is my settlement still DRAFT?
DRAFT is a working result while the pipeline resolves data and can be re-run safely. As data becomes usable it can be PRELIMINARY; FINAL is the controlled close, so the normal runner does not silently clobber it. Check the period’s readings and pipeline status before you retry.
→ V8 §11Why can’t I re-settle a FINAL period from the nightly job?
FINAL is an operator-protected statement of the period, not an automatic cache. The runner may update DRAFT or PRELIMINARY work but requires the explicit correction/re-settlement path for FINAL. Identify the source correction first; changing a tariff or meter row without that trace is not a safe remedy.
→ V8 §11Why is a statement not issued?
StatementRunneris enqueued by settlement finalisation, so a non-FINAL period has no normal statement to issue. Confirm the period status and inspect thesettlementqueue before assuming a money calculation failed. A failed statement job needs its job evidence and retry path; it does not authorize editing ledger rows.
→ V8 §9 · §3.4Why is the result empty after a re-run?
The empty-run guard prevents a partial-meter tail from wiping a previously useful result. Treat the empty output as a data-completeness signal, then inspect the interval window and allocation inputs. Once the readings land, use the owned re-settlement route rather than fabricating zero values.
→ V8 §11Why do IPP hours say provisional?
Provisional means the calculation has incomplete or late meter evidence, not that the platform has lost energy. Late cumulative reads preserve the quantity and allow a controlled re-settlement. Compare the interval coverage first, then read the final report after data is complete.
→ V4 §8 · V8 §3
Deals and lifecycle
Why did my proposal expire?
The hourly AshOban:expire_staletrigger moves a PENDING proposal pastexpires_atto EXPIRED. That is a lifecycle transition, not a deleted negotiation. Renew only when its product andmax_renewalsallow it; otherwise create the appropriate new deal flow.
→ V7 · §3.5Why is my contract still SIGNED after its start date?
The daily PhysicalPpa:suspend_unsignedtrigger (and the virtual twin) moves AGREED deals past the 10-day sign window to SUSPENDED; revival is a mutual Request / Accept on Negotiations (V7 §5.1). The daily PhysicalPpa activation trigger moves SIGNED to ACTIVE whenstart_date ≤ today. Confirm the date and the trigger/queue evidence before changing contract data. If the lifecycle is valid but execution failed, follow the deal owner’s recovery route rather than backdating settlement.
→ V7 · §3.5
Meters, jobs, and live data
Why is an interval missing or red on the gallery device?
CODEX-DEMO-GALLERY-MISSING-READdeliberately contains a gap so you can recognize incomplete coverage. It is expected fixture data, not a reason to repair the demo database. The resolution rules preserve energy when a later cumulative read arrives.
→ V2 §7Why is an Oban queue stuck?
First identify the worker and its owning queue—such assettlement,brp,strategy,forecast, orsetup—then inspect its error and retry state. A stuck job usually reflects an unmet input or external boundary, not an invitation to create a duplicate job. Use the worker card to verify what it should have produced before retrying.
→ §3Why did the IPP monthly forecast not fan out?
MonthlyForecastRunner runs at 04:00 on the fifth and fans out a ForecastRunner per eligible active IPP. Check the active physical PPA and theforecastqueue before expecting a report. A missing fan-out is not corrected by manually copying another IPP’s forecast.
→ §3.3 · V4 §8Why are live ticks colliding with setup?
A mutating profile and S15 would write incompatible assumptions into the same world, so setup refuses the overlap. Stop the simulator withWB.Setup.stop_live(), verify it stopped, then resume or rebuild from §4. Do not force a reset through the guard.
→ §4 · §6
Market and Scenario Lab
Why is a bid EXPIRED or showing a gate-miss?
The bid was built after the D−1 gate determined by MarketRules; 10:00 is a typical gate, not a substitute for configured rules. Check the gate, worker timing, and bid source before attempting another submission. A retry after delivery has moved on is not a valid bid.
→ V9 §4Why is the SIMULATED ribbon visible on MO or SO screens?
A Scenario Lab run still owns those artifacts, and its status is notTORN_DOWN. Open/org/sim, identify the run by its name and seed, then inspect or tear it down when the exercise is complete. Do not read a ribbon-marked screen as production-like operational evidence.
→ V14 · §5 · V3 §17Why does a Lab comparison show a drifted badge?
The two runs no longer share comparable provenance or resulting trajectory: confirm scenario clone, seed, period, and edited controls in the day log. A baseline comparison should isolate one intended change, such as a day factor, rather than blend market, SO, and MO injections. Clone a clean baseline and rerun if you cannot explain the divergence.
→ V14 §10
Setup and seats
Why did setup fail mid-stage?
Inspect the failed stage and itssetupqueue evidence, then resume the same manifest ID after correcting the cause. A second--reseton a half-built world removes the audit trail that explains the failure. Resume that manifest; do not start a second reset.
→ §4Why is my password wrong after a rebuild?
Demo seats usesupersecret; Eskom iseskom@webill.net, and a municipal account is its normalised municipality name plus@webill.net. If the expected seat cannot sign in, correct the S01 pack and rebuild. Do not hand-edit password hashes, because that breaks reproducibility.
→ §4Why can’t I find Scenario Lab on the landing page?
The Lab requires compile-time and runtimesim_enabledgates. Production builds omit it, and a disabled runtime redirects the route home. Use a development/demo build and the lab/full-demo setup profile rather than attempting to expose the route manually.
→ V14 · §5
Eskom and Munix
Why is the municipality map the wrong colour?
The Munix Configuration wheeling-status chip drives the V1 map legend. Confirm whether the published state should be Wheeling, Pilot, Partial, or None, save it in the municipality seat, then reload the organisation map. A colour is not proof that a tariff or PPA exists.
→ §2 · V2 §9Why does Munix say Wheeling but the map still seems unusable?
The status publishes municipal participation, while the map presents that fact to organisation users. It does not create an Embedded wheeling tariff, Monthly Admin Fee, valid site configuration, or agreement. Check the saved Munix charge controls and then trace the V1 site/deal path.
→ §2 · V2 §9Why is Permutate disabled or doing nothing?
Every eligible charge has already been permutated or activated, so there are no missing season/TOU shells to create. Open Charges and Rates to confirm the grid and saved values; the action never invents a price. Activate only after the approved grid is complete.
→ §1
The health probe fails, but the home page opens
Open /health/ready and read checks. A 503
identifies a critical dependency failure; /health/live only
proves that the endpoint answers. Restore the named dependency or apply
pending migrations. A missing HiGHS binary or stale weather produces
degraded instead. An unconfigured /metrics
returns 404 by design; configure the bearer token on the web listener
and send it in the Authorization header.
8. The demo dashboard —
/demo
One room for the whole demo estate. Everything §4 builds — the full demo world, the simulated SAWEM market, traded Billing Periods — is seeded, watched and kept up to date from this page, and the inventory of what was built is listed on it. The three demo tools the landing page used to host live here now.
8.1 Opening it
From the landing page (/), the Demo
tools strip shows the full demo’s seal state in one line;
Open Demo Dashboard opens the room in its own window,
so it can sit beside the console you are demonstrating. The page exists
only where demo tools are enabled (:sim_enabled — dev and
demo builds); on any other build the route is compiled out and a stray
visit lands back on /. No seat is required — like the
Scenario Lab (§5), it is a harness, not an
organisation login.
Follow along. Any dev/demo build. Path:
/→ Open Demo Dashboard. You should see: the world’s identity in the header (as-of date, seed, today), eight stat cards, four operation cards, the last three Billing Periods, the daily checks, and the tables of what was built.
8.2 The operations
Four cards. Every mutating button states in its confirmation exactly what it will do; every run happens in the background and the page stays usable.
| Card | What it runs | Notes |
|---|---|---|
| Full demo data | full_demo (S00–S14) — Seed full demo
when the world has never been sealed. When it is sealed on an older
clock, Update demo to today runs §4.4 Procedure D
(WB.Setup.roll_forward/1) in the background: same seed,
as_of becomes today, derived strategy/settlement data
rebuilds. The button shows Updating… and stays disabled
until the new seal. |
Resets application tables; several minutes. The simulated market and traded months are not rebuilt — use those cards (or Set up everything) for the whole estate. Catch up metering now (§8.4) is the non-destructive meter fill. |
| Simulated SAWEM market | S16 — the fifty-participant overlay of §4.7, without teardown | Also links to the Scenario Lab |
| Market trading | S17 — §4.8, for the picked Billing Period or Trade the window months — the two closed months settled, the current month traded through today and left unsettled | Requires the overlay; a part-seeded month resumes rather than duplicating |
| Set up everything | The whole estate from scratch, in one sequence: teardown, the full demo with the market overlay in the same run (S00–S14 + S16), the two closed Billing Periods settled, the current month traded through today, then a day-ahead backfill so every participant has submissions through tomorrow’s gate | Nothing is skipped — after the teardown nothing is built. The console equivalent is printed under the button |
8.3 Live progress
Progress is read from the rows the seeds write — the
setup manifest, the published clearing runs, the statements — so it
survives a reload, a second browser and a restarted server. Setup
transitions additionally ring a PubSub doorbell
(WB.Setup.PubSub) so every open dashboard refetches the
moment a stage completes or a Trading Day lands, with a slow poll as the
fallback while anything is running. If the page and the rows ever seem
to disagree, believe the rows and reload.
8.4 Daily start-up checks
The estate has two daily obligations: the 20-minute meters must reach now, and every simulated participant must hold tomorrow’s day-ahead series. The Daily start-up checks panel owns both.
- Run at boot — default on. When on,
a boot-time runner (dev/demo builds,
:demo_startup_checks) performs both checks shortly after the application starts. The switch is stored insetup_audit.demo_settings, the schema that survives teardown — a rebuild does not reset it. When off, the two buttons below are the only way the checks run. - 20-minute metering — how many metered PPA sides exist, the latest stored reading, and how many intervals behind now that is. Catch up metering now starts the live simulator through the S15 profile against the sealed world’s own clock and seed (§6); the simulator then back-fills every missing interval and settles into the live cadence. A running simulator needs no button — catching up is what it does.
- DAM submissions — the next Trading Day, whether every unit has submitted for it, and any recent Trading Days (three-month horizon — the same window the trading-month tools operate over) where a series is missing — the hole the daily 03:00 submitter (§4.7) never refills on its own. Backfill DAM now submits the missing days, oldest first; units already holding an operative series are skipped.
8.5 What was built
The stat cards and tables are the §4.2
world, counted and named: organizations with their BRP registrations and
holdings (simulated participants badged sim), generator
sites by technology and connection, off-taker sites by category, trader
pools, and every physical and virtual PPA as a contract
leg (who sells to whom — site, pool, organisation) beside its
network side (Eskom, or the municipality each end sits
in). Document counts split wheeling billing (tax invoices, credit notes)
from market documents (invoices, self-billing, debit notes), and carbon
certificates are summed from issued batches.
8.6 Safety rails
The §4.1 rails all hold here: dev/demo
builds only, production refused below the page by the setup control
plane, destructive actions confirmed in plain words, one setup at a time
(a second Set up everything is refused while one runs),
and the page itself writes nothing — every mutation goes through the
same WB.Setup functions the mix tasks call.
8.7 Market phase before a demo
The phase shown on this dashboard is the register
value, not a build flag. Changing it is a publish on
Market Operator → Rules (/mo/rules) —
open, review, confirm — the same ceremony as moving the price cap. It is
not a deployment.
Before a sales demo, open /demo and / and
/mo/rules and check that the three labels agree with each
other and with what you are about to say in the room. A rebuilt demo
estate seeds Phase 1 explicitly
(WB.Setup.Packs.Market.Registrations,
WB.Setup.Packs.Market.Simulated). Walkthrough: V9 §18.
Where next. Procedures and recovery: §4. The overlay’s cast: §4.7. What a traded month writes: §4.8. Watching live ticks: §6. What-if experiments on the sealed world: the Scenario Lab, V14.
Trading-control status
The daily status report includes armed registrations, PROPOSE drafts awaiting submission, individual check refusals in the last 24 hours, and approvals approaching expiry. Counts come from persisted rows. A refusal count counts check results, so one bid refused by two checks contributes two. Review the Controls tab for the affected bid and the Approvals inbox for decisions.
9. Registry connector operations
The carbon registry seam (WB.Carbon.Registry.Connector)
is Mock in every committed environment. Live Verra and Gold Standard
adapters exist; going live is a configuration change,
not a code change.
Credentials
Environment variable names only — never values in
config/*.exs:
| Adapter | Key | Base URL |
|---|---|---|
| Verra | VERRA_REGISTRY_API_KEY |
VERRA_REGISTRY_BASE_URL |
| Gold Standard CorTenX | GOLD_STANDARD_API_KEY |
GOLD_STANDARD_BASE_URL |
Keys belong in KMS (spec §17), not in the repo and not in a shell history that gets pasted into a ticket.
Fixture capture
- Point the adapter at a sandbox (or a live account you control).
- Run each of the five callbacks once.
- Sanitise: redact account identifiers and personal data; keep field names and shape.
- Commit under
test/support/fixtures/registry_connectors/<adapter>/with the capture date and API version in that folder’s README. - CI replays the files through the real
Reqdecode path with:carbon_registry_forbid_networkset.
When a registry changes its API, re-capture. Production already stores every live response as an artifact, so drift shows up in data, not in guesswork.
Go-live checklist
- Conformance suite green (
verra_conformance_test.exs,gold_standard_conformance_test.exs). - Config map selects the live adapter for that registry only.
- Credentials present at runtime; Mock remains the default for every other registry.
COAS manual procedure
COAS has no public API in v1. Operators complete each stage by
uploading the document (hashed through Artifact) and
capturing the reference:
| Stage | What to upload | Reference |
|---|---|---|
| LoA applied | Application pack | DEE/COAS application id |
| LoA granted | Extended Letter of Approval | LoA number |
| Cancellation requested | Origin-registry cancellation request | Origin reference |
| Cancelled | Cancellation certificate (purpose wording §2.4) | Cancellation certificate id |
| COAS listed | Listing confirmation | COAS ownership-repository id |
| COAS transferred | Transfer confirmation (when it happens) | Transfer id |
| Retired | SARS Carbon Offset Certificate | RetirementClaim.coas_reference |
A stage cannot complete without its artifact. A stage cannot skip its predecessor. After Cancelled, international-side actions refuse.
CAD Trust observer
(/operator/cadt)
The CADT node is infrastructure beside the app,
never an Elixir dependency. Hosting is invisible to code. CI and the
demo estate run the Mock exclusively (:cadt_forbid_network
in test).
/operator/cadt is the observer console: per-registry
sync health (roots, last synced, :STALE with the recorded
error) and the suspect ledger (:OPEN first, resolution
trail on the resolved). Observation reads; this screen never holds keys
and never writes to the node.
A genuine double-count hit blocks listing and retirement until an operator resolves it with an evidence artifact. The Certificate Desk shows the same incident as a blocking banner.
Demo step S22 seeds one colliding serial block
against the Mock’s planted fixture, runs the sweep, and resolves the
suspect so a fresh mix setup.demo always shows a
found-and-resolved trail.
CAD Trust node — deployment, mirror, keys
The Connectivity Deck’s sizing for a CADT node plus Chia: quad-core,
4–8 GB RAM, ~500 GB disk, Ubuntu preferred. Optionally run CADT’s
mirror database (MySQL/Postgres) as the
analytics-friendly read path. The app talks to either through
WB.Cadt.Client; swapping Mock for NodeApi (or
a later mirror adapter) is configuration.
Testnet is the default. No committed config names
mainnet. Publishing is the same software with a Chia wallet key (24-word
mnemonic in real key management — it signs data-layer writes
inside the node) plus a CADT API key that merely guards
the node’s API. The API key reaches the adapter from
CADT_API_KEY / CADT_BASE_URL at runtime. The
adapter refuses publishing with :missing_api_key rather
than defaulting.
The key ceremony itself is an ops runbook outside this repository. Record who performed it, when, and which KMS slot holds the material. Never paste a mnemonic, API key, or example secret into tickets, fixtures, or this manual.
10. Platform health and metrics
The Demo dashboard’s daily checks include Platform
health, with a link to the complete readiness report.
/health and /health/ready work on both
listeners without a market token. ok and
degraded return 200; a critical failure returns 503.
/health/live always returns 200 when the endpoint can
answer, so it is the process liveness probe.
The eight checks cover the database, pending migrations, TimescaleDB, Oban, the Oban leader, HiGHS, weather freshness and cluster peers. Each has a two-second deadline. An optional fault (such as a missing solver or stale weather) is degraded; stopped Oban, a failed database, pending migrations or missing TimescaleDB fail readiness. Mock feeds identify themselves as mock. These checks make no external provider request.
PromEx records web/API request latency, LiveView activity, database queries, jobs, VM usage, publication and gate-miss counts, API authentication/idempotency/rate-limit outcomes, budgets, feed/FX ages, leadership and readiness. Clearing metric names are registered for the later background-clearing milestone; they do not yet have emitters.
/metrics is available only on the web listener. It
returns 404 until operations supplies METRICS_BEARER_TOKEN,
then requires a matching bearer token (401 otherwise). Restrict the load
balancer path to the Prometheus host and scrape each instance every 15
seconds. Grafana dashboard upload needs its configured host and
credential; the application does not upload dashboards at boot. Never
put tokens into links or the manual.
Honeybadger uses its environment key and the wb_wheeling
app name. Moving a key out of a file does not rotate it: the operator’s
dated rotation and revoked-key check live in
docs/references/ops/DEPLOY-EC2.md. Stage 1’s rotation is
still pending.
11. Feeds and delivered events
The operator’s problem
Webill does not invent every number it shows. Meters arrive from files and devices. Weather arrives from a forecast vendor. Carbon registries answer when you ask about a certificate. The market gateways send bids out and bring results back. A buyer may ask you to knock on their own server when an invoice is issued.
If any of those pathways goes quiet, the screens still look finished — they just show yesterday. The operator’s problem is therefore simple: name every pathway, know how often it should speak, and see when it last told the truth.
A named pipe, then the platform word
Think of the platform as a house with labelled pipes. One pipe carries 20-minute meter buckets. Another carries hourly weather. A third carries an outbound “invoice issued” knock to a URL you typed. You do not pour hourly rain into the meter pipe, and you do not judge a weekly registry check by a 20-minute clock.
That labelled pipe is a feed. The label on the pipe is the family. How often a reading is supposed to arrive is the grain (for example 20 minutes). How the pipe is allowed to run — live, pretend, or only when a person uploads — is the mode. The house rule for “this is still current” is the policy. Each try to use the pipe is an attempt. What you see on the register — fresh, stale, never, refusing — is the status, derived from those facts. It is not a mood.
A 20-minute meter feed does not redefine an hourly weather forecast. Grain belongs to the feed.
11.0 Words you need before the buttons
| Plain English | Platform word | One-sentence meaning |
|---|---|---|
| A labelled pipe in or out of the house | Feed | One named pathway: provider, family, grain, mode, and a policy |
| The kind of thing the pipe carries | Family | METER, TELEMETRY, WEATHER,
PRICE, FX, REGISTRY,
BILL, MARKET, or EVENT |
| How big each bucket is | Grain | The interval of one reading, in minutes (20 min,
60 min, or — when the feed is not
interval-shaped) |
| How often we bother the pipe | Cadence | Seconds between scheduled tries (a weather pull every 21,600 seconds is six hours) |
| In or out | Direction | INBOUND (data arrives) or OUTBOUND (we
send) |
| Live, pretend, or by hand | Mode | LIVE, MOCK, or MANUAL |
| The house rule for “still current” | Policy | An effective-dated freshness window, optional quota, and a source note |
| One try to use the pipe | Attempt | A timestamped OK, REFUSED,
TRANSIENT, or OUTAGE row |
| Whether the pipe is telling the truth now | Status | NEVER, FRESH, STALE,
REFUSING — or the mode badges MOCK /
MANUAL |
| “Please knock on my door when X happens” | Webhook | An HTTPS callback you register for a topic |
| The post that could not be delivered | Dead letter | A delivery that exhausted retries or was abandoned; it waits for a human Retry |
| The one-time door key | whsec_ secret |
A signing secret shown once; it is never put in the payload |
Abbreviations this chapter uses
| Abbreviation | Stands for | Here it means |
|---|---|---|
| HTTPS | Hypertext Transfer Protocol Secure | A web address that starts with https:// — the only
callback the form accepts |
| URL | Uniform Resource Locator | The address of that callback |
| HTTP | Hypertext Transfer Protocol | The numbered reply from the callback (200 means
success; 410 means gone) |
| FX | Foreign exchange | Currency rates used by regional trading |
| CSV | Comma-separated values | A spreadsheet-shaped text file (meter imports and plant historian mirrors) |
| AMI | Advanced Metering Infrastructure | A municipal smart-meter export format |
| MV90 | A common meter-export format | The Eskom interval-file shape the import adapter reads |
| MRV | Monitoring, reporting and verification | Carbon-project device readings — not settlement meters |
| CAD Trust / CADT | Climate Action Data Trust | An observer feed for carbon registry facts |
| COAS | A South African carbon-offset registry path | Tracked by hand (MANUAL); there is no live pull |
| SAPP | Southern African Power Pool | Cross-border market client and hydrology |
| SO | System Operator | The grid controller; forecast packs and curtailment claims go out on
MARKET feeds |
| MO | Market Operator | The market clerk; the organisation talks to it on
market:mo_gateway |
| BRP | Balance Responsible Party | The organisation that must match what it scheduled to what the meters did |
| SAST | South Africa Standard Time | UTC+2. Screen times you care about are SAST; some night jobs are catalogued in UTC |
| UTC | Coordinated Universal Time | The scheduler’s clock in §3 |
| Oban | The job runner | Background work: the fifteen-minute sweep, webhook delivery, retention |
| SSE | Server-Sent Events | The Market API’s live event stream — a different pipe from these webhooks |
Where this is taught. Night-job times live in §3. Plant-owner screens live in V4 §11 and V2 §11. Signed Market API subscribe/retry endpoints live in V13 §13.
How a reading becomes a status
flowchart TB
A[Something arrives or is sent on a named feed] --> B[Record one attempt: OK, REFUSED, TRANSIENT, or OUTAGE]
B --> C[Update last attempt and last success]
C --> D[Apply today's policy]
D --> E[Derive NEVER, FRESH, STALE, or REFUSING]
E --> F[Fifteen-minute sweep shows the new badge and files a finding if it just went stale or refusing]
- A worker, an upload, or a plant push uses a feed
key (for example
weather:mockorplant:{site}:{provider}:{start-date}). - The platform writes an attempt with an outcome.
- If the outcome is
OKand the clock is newer than the last success, last success moves forward. - Status is then derived — it is not stored as a separate opinion.
- Every fifteen minutes
FreshnessSweepre-seeds the catalog, recomputes every badge, broadcasts the change, and files a quality finding when a live feed has just becomeSTALEorREFUSING.
Attempt history is kept for ninety days, and each
feed always keeps its latest attempt even after that window. That job is
AttemptRetention (01:10 UTC) — see §3.
Mode is not status
MOCK and MANUAL are modes.
They never pretend to be live freshness.
| Mode | Everyday picture | What the badge means |
|---|---|---|
| LIVE | The real postman | Status is derived from policy and attempts: NEVER,
FRESH, STALE, or REFUSING |
| MOCK | A practice pipe in the basement | Always shows MOCK, even when idle. Demo weather and the
live-meter simulator sit here |
| MANUAL | You walk the letter to the counter | Always shows MANUAL. Meter file imports and bill
uploads sit here — nothing is late until a person acts |
A live feed without a policy is already STALE,
reason no_policy. A missing rule is itself a stale
condition. — on grain or last success means unknown / never
succeeded, not a third clock.
NEVER — no successful attempt yet. FRESH — last success still inside the policy window. STALE — window expired, or there is no policy. REFUSING — the latest attempt is REFUSED and is newer than the last success. A later OK returns the feed to FRESH.
| Status | Plain meaning | How you get there |
|---|---|---|
| NEVER | This live pipe has never delivered a success | last_success_at is empty |
| FRESH | The last success is still inside the policy window | A clock no older than freshness_window_seconds after
last success |
| STALE | We should have heard by now — or nobody wrote a rule | Window expired, or no policy row |
| REFUSING | The far side said no, after we had once succeeded | Latest attempt outcome is REFUSED and is newer than
last success |
| MOCK | Practice data | Mode is MOCK — status is not computed |
| MANUAL | A person must act | Mode is MANUAL — status is not computed |
Default policies (source note GD6 interim / F12 platform defaults, effective from 1 January 2020):
- Meter families — 2,400 seconds (40 minutes: two 20-minute grains).
- Everyone else — 21,600 seconds (six hours).
- Weather quotas — 10,000 calls per 86,400 seconds,
except
weather:solcastat 500 per day.
Policies are effective-dated. A new row with a later
effective_from replaces the old rule without rewriting
history.
Each feed family
The catalog has nine families. Eight have named pathways on the operations register today. PRICE is reserved: the enum exists; no seeded row uses it yet.
| Family | Everyday picture | Direction | Typical grain | Who uses it |
|---|---|---|---|---|
| METER | The household electricity meter | In | 20 min | Settlement and the live simulator. Not the plant “now” panel |
| TELEMETRY | A plant’s own instruments, or a river gauge | In | 5–10 min on a plant; — on hydrology / carbon MRV |
Owner plant screen; SAPP hydrology; carbon MRV. Not settlement |
| WEATHER | The outdoor thermometer / forecast | In | 60 min | Strategy and IPP forecasts |
| PRICE | A market-price ticker | — | — | Reserved; no registered pathway |
| FX | The bureau-de-change board | In | — (daily cadence) |
Regional trading |
| REGISTRY | The carbon certificate card catalogue | In | — |
Carbon desk; CAD Trust observer; COAS by hand |
| BILL | A PDF or CSV you upload | In | — |
Billing ingest |
| MARKET | The clerk’s counter at the market | In or out | 60 min on the gateways | MO / SO / SAPP clients; SO forecast pack; curtailment claim |
| EVENT | “Knock on my door when X happens” | Out | — |
Signed webhooks |
METER. Interval energy used to settle contracts. Imports are MANUAL (you drop a file). The live simulator and device ingest are scheduled. Setup backfill is MOCK. These rows are the settlement truth. A plant telemetry sample never replaces them.
TELEMETRY. Device evidence: power, charge, wind speed, hydrology, carbon MRV. Three shapes exist:
- Platform rows —
telemetry:hydrology(SAPP) andtelemetry:carbon_mrv. - Per-plant rows — created when an owner registers a
feed (
plant:{site-id}:{provider}:{start-date}). See §11.2. - Not this family — carbon inverter project telemetry on a methodology pack, and 20-minute settlement meters.
WEATHER. Hourly forecasts. Demo rows resolve to the Mock provider until weather is armed. Open-Meteo, the Open-Meteo ensemble, and Solcast are named so a later go-live is a configuration change, not a new pipe.
PRICE. No key, no row, no policy. Do not invent a ticker here.
FX. Daily foreign-exchange rates. Demo uses
fx:mock.
REGISTRY. Verra, Gold Standard, the mock connector, CAD Trust, and COAS. COAS is MANUAL — a person tracks it. Live Verra / Gold Standard adapters exist; going live is taught in §9.
BILL. bill:manual_upload — you bring a
bill. Nothing is stale because a person did not upload today.
MARKET. How the organisation talks to the Market Operator, the System Operator, and SAPP, plus two outbound duties: the hourly Grid Code forecast pack and a curtailment claim. Gateways are 60-minute grain with a 15-minute cadence (900 seconds).
EVENT. One outbound row,
events:webhook_out. Every signed delivery records an
attempt on this feed. The human workflow is §11.1.
The named pathways on the register
These keys are seeded by WB.Feeds.Registrations. The
Pathway column shows the provider (today that string
is the key) and the resolved module.
| Key | Family | Mode (typical demo) | Grain | Cadence | What it is |
|---|---|---|---|---|---|
weather:mock |
WEATHER | MOCK | 60 min | 6 h | Practice forecast |
weather:open_meteo |
WEATHER | MOCK until armed | 60 min | 6 h | Open-Meteo |
weather:open_meteo_ensemble |
WEATHER | MOCK until armed | 60 min | 6 h | Ensemble spread |
weather:solcast |
WEATHER | MOCK until armed | 60 min | 6 h | Solcast (quota 500/day) |
registry:verra |
REGISTRY | MOCK until armed | — | — | Verra connector |
registry:gold_standard |
REGISTRY | MOCK until armed | — | — | Gold Standard |
registry:coas |
REGISTRY | MANUAL | — | — | Hand-tracked COAS |
registry:mock |
REGISTRY | MOCK | — | — | Practice registry |
registry:cadt |
REGISTRY | from adapter | — | daily | CAD Trust observer |
meter:import:canonical |
METER | MANUAL | 20 min | — | Canonical interval import |
meter:import:eskom_mv90 |
METER | MANUAL | 20 min | — | Eskom MV90 export |
meter:import:municipal_ami |
METER | MANUAL | 20 min | — | Municipal AMI CSV |
meter:import:mock |
METER | MANUAL | 20 min | — | Practice import |
meter:setup_backfill |
METER | MOCK | 20 min | — | Demo history writer |
meter:live_simulator |
METER | MOCK | 20 min | 20 min | S15 live ticks |
meter:device_ingest |
METER | LIVE / ingest | 20 min | 20 min | Device → time series |
telemetry:hydrology |
TELEMETRY | from adapter | — | daily | SAPP hydrology |
telemetry:carbon_mrv |
TELEMETRY | from adapter | — | daily | Carbon MRV |
fx:mock |
FX | MOCK | — | daily | Practice FX |
market:mo_gateway |
MARKET | from adapter | 60 min | 15 min | Market Operator client |
market:so_gateway |
MARKET | from adapter | 60 min | 15 min | System Operator client |
market:sapp_client |
MARKET | from adapter | 60 min | — | SAPP client |
market:so_forecast_submission |
MARKET | outbound | 60 min | — | Grid Code forecast pack |
market:curtailment_claim |
MARKET | outbound | — | — | Curtailment claim channel |
bill:manual_upload |
BILL | MANUAL | — | — | Bill ingest |
events:webhook_out |
EVENT | outbound | — | — | Signed webhook deliveries |
plant:{site}:{provider}:{from} |
TELEMETRY | MOCK or LIVE | cadence ÷ 60 | as registered | One owner’s plant feed |
Timezone: most rows require an explicit UTC offset on timestamps
(utc_offset_required). Import adapters may declare their
own rule.
Data. Catalog from
WB.Feeds.Registrations.rows/0as of manual 3.20.2. Demo modules resolve to Mock until a provider is armed. Last checked 2026-09-07.
How to use the Feed
registry (/ops/feeds)
This is the operations estate, not an organisation
login. It exists only where demo tools are compiled
(:sim_enabled — dev and demo builds). On any other build
the route is not there; a stray visit is sent back to /. No
seat is required. There is no menu chip on the Demo dashboard today —
you type the address.
Follow along. Any dev/demo build. Path: open
/ops/feeds. You should see: heading Feed registry, a Family or status filter, a table (#feed-rows), and a Delivered events → link to/ops/events.
| Column | What it shows | What you do with it |
|---|---|---|
| Pathway | Provider (the key) on the first line; family and resolved module underneath | Confirm which pipe and which code is plugged in |
| Mode / status | Amber mode chip (LIVE / MOCK /
MANUAL) plus the derived state; a “since” line when the
state has a start |
MOCK/MANUAL: ignore freshness. LIVE: act on NEVER / STALE / REFUSING |
| Grain | 20 min, 60 min, or — |
— is “not an interval feed”, not an error |
| Last success | Timestamp or — |
— on a LIVE row is NEVER |
| Policy | Freshness {n}s · quota {n or unmetered} · {source note},
or No policy |
No policy on a LIVE row is already STALE |
Filter (#feed-filter, label Family
or status): All, each family, or MOCK /
MANUAL / STALE / FRESH /
NEVER / REFUSING. The filter is exact.
WEATHER does not show plant telemetry.
The table live-updates when the sweep broadcasts a status change. You do not create feeds on this page. You read them. Creating a plant feed happens on the owner’s plant screen. Creating a webhook happens on Configuration or the Market API page.
Done looks like: every LIVE row you care about is
FRESH, MOCK rows still say MOCK, MANUAL rows are idle
unless someone should have uploaded, and no LIVE row says No
policy.
Empty / refusal. An empty table after a filter means
nothing matches — the pipes are still there. Production has no
/ops/feeds at all.
How to use Data feeds on Configuration
The Feeds & webhooks tab of the Organization
room (/org/configuration/feeds, V2 §2.6) stacks two
cards in #organization-data-fabric:
- Data feeds — every feed you own, plus the shared METER and WEATHER rows (those have no organisation owner; every org may see them).
- Webhooks — §11.1.
Each feed row gives a plain name with its key under it (for example
Meter devices, meter:device_ingest), the
kind of data, a status chip and the interval (20 min, or
— when the feed has none). The chip translates the status:
| Chip | Status |
|---|---|
| Receiving | FRESH, with the time of the last good reading |
| Stale | STALE, with the time it went stale |
| No data yet | NEVER |
| Refused | REFUSING |
| Upload on demand | MANUAL |
| Simulated | MOCK |
The same strip is attached to the Market API page
(/org/brp/api) when a participant registration is selected.
There the webhook scope is the party, not the
organisation. Pick a registration first or Create webhook refuses with
No participant registration selected.
Follow along. Seat:
offtaker_c@webill.net(Organization; passwordsupersecret). Path: sign in → Configuration → scroll to Your feeds. You should see: shared weather/meter lines and any org-owned plant feeds, each with mode and status.
11.1 Webhooks and dead letters
A webhook is a promise: “when this kind of thing happens, knock on this HTTPS address and sign the knock so I know it is you.” The signed knock is a delivery. If the far door never opens, the letter is not thrown away — it becomes a dead letter and waits for a person.
That is the outbound EVENT feed
events:webhook_out.
What they are for
Think of a doorbell on your office, not a letterbox on ours. You do not come here to fetch every invoice. You hang a bell on your finance system (or your ERP, or a chat room that can take HTTPS). When Webill issues an invoice, we press that bell. Your system opens a payable without anyone staring at this screen.
A webhook is therefore for telling another system you already run. It is not a feed into Webill. We do not invent the address — you paste an HTTPS URL you control. The Webhooks card on Configuration says so under its title.
The kind of news you asked for is the topic (for
example invoice.issued). One webhook is one URL plus the
topics you tick. A URL holds one webhook; to change what it hears, edit
its topics rather than adding a second row.
A worked example —
invoice.issued
OffTaker C buys wheeled energy. When the seller’s bill is issued, C’s finance clerk should not have to open Configuration and hunt. Their accounts-payable system should already have a line: document number, amount, due date.
- On Organization → Feeds & webhooks, under
Webhooks, C pastes
https://finance.offtaker-c.example/webill/hooksand leaves the default topicinvoice.issuedticked. - They press Create webhook, press
Copy beside the
whsec_secret, paste it into the finance app (so it can check the signature), then press I’ve stored it. - Later, a billing document for C is issued. Webill POSTs a small
signed JSON body
{ type, timestamp, data }withdocument_idandnumber. - Finance answers 200. Recent
deliveries shows
invoice.issuedwith a Delivered chip and 1 attempt. The EVENT feed records anOKattempt. - If finance is down, we retry (see below). After a dead letter, C presses Retry once the server is back.
flowchart LR
A[Seller issues C's invoice] --> B[Webhook invoice.issued]
B --> C[POST to C's HTTPS finance URL]
C --> D[Finance opens a payable]
D --> E[Finance answers 200]
E --> F[Recent deliveries shows Delivered]
Follow along. Seat:
offtaker_c@webill.net(Organization; passwordsupersecret). Path: Organization → Feeds & webhooks → Webhooks. You should see: the help line under the title,invoice.issuedticked, and Create webhook. Demo/dev can use the in-process receiver instead of a real finance host (§11.1 local receiver).
Data. The knock for this topic is wired: when a billing document is issued, both the issuer organisation and the recipient organisation are notified (
WB.Events.Notifier). The table below says which topics knock, and where.
Each topic and why you would use it
Every topic name is listed here. Why subscribe is the job you would give your other system. Knocks from Your webhooks is what is built and wired today: only four topics are published into this doorbell.
- Configuration (organisation scope) offers only the
three wired organisation topics:
invoice.issued,credit_note.issuedandsettlement_period.final. Each carries a one-line description. - The Market API page (
/org/brp/api, party scope) offers the whole list, so the names match the Market Code / Market API vocabulary. A subscriber there can wait forever until a publisher is added. Some of those names can already be read by polling the Market API (V13 §13) — that is a different pipe from this POST.
Money and paper
| Topic | What happened | Why you would subscribe | Knocks from Your webhooks |
|---|---|---|---|
invoice.issued |
A bill was issued to or by your organisation | Open a payable / receivable in finance; start a payment run | Yes — issuer and recipient |
credit_note.issued |
A credit note was issued | Reverse or reduce that payable / receivable | Yes — issuer and recipient |
settlement_period.final |
A physical PPA’s settlement period was finalised | Lock the month in your ledger; stop treating it as draft | Yes — seller and buyer organisations on that PPA |
statement.issued |
A market settlement statement was published | Pull the statement into your books | Form only |
document.received |
A document arrived for you | File it; start a review | Form only (API poll may already show it) |
bill.updated |
An existing bill changed | Refresh the payable; do not pay the old figure | Form only |
The market day
| Topic | What happened | Why you would subscribe | Knocks from Your webhooks |
|---|---|---|---|
gate.closing |
A bid gate is about to close | Page the trader; freeze last edits | Form only |
gate.extended |
That gate was given extra time | Unlock the desk; you still have a window | Form only |
submission.rolled_over |
Your bid was carried to the next day | Confirm you are still in; do not re-type a live bid | Form only (API poll may already show it) |
schedule.published |
The market published your schedule | Drive the plant / book to that shape | Form only (API poll may already show it) |
prices.published |
Day-ahead prices were published | Reprice positions; update a public board | Form only (API poll may already show it) |
idm.cleared |
The intra-day market cleared | Apply the afternoon correction | Form only (API poll may already show it) |
balancing.prices.published |
Imbalance prices were published | Estimate the balancing bill | Form only (API poll may already show it) |
dispatch.instruction |
The System Operator instructed your unit | Move output; log the instruction in ops | Yes — the party’s registration (use
/org/brp/api, not Configuration) |
Credit cover and notices
| Topic | What happened | Why you would subscribe | Knocks from Your webhooks |
|---|---|---|---|
credit_cover.increase_notice |
You were asked to post more cover | Start a treasury transfer | Form only (API poll may already show it) |
credit_cover.warning |
You are near the warning limit | Alert treasury before a call | Form only (API poll may already show it) |
credit_call.made |
A credit call was made | Treat it as urgent cash | Form only |
notice.published |
The Market Operator published a notice | Put it on the trading desk | Form only (API poll may already show it) |
query.resolved |
A data query was resolved | Close the dispute file | Form only |
Standing data
| Topic | What happened | Why you would subscribe | Knocks from Your webhooks |
|---|---|---|---|
registry.counterparty_change |
A party’s registry facts changed | Refresh KYC / standing data | Form only (API poll may already show it) |
dispatch.instruction is scoped to a
party (your market registration). Create it on
/org/brp/api after you select a registration. The other
three wired topics are scoped to the organisation and
belong on Configuration.
Create a webhook
On Configuration (organisation scope) or /org/brp/api
(party scope):
- Enter an HTTPS callback URL. An
http://address, or one with a user name or password in it, is refused at the field. - Tick one or more Topics. None ticked is refused at the field.
- Press Create webhook
(
#create-webhook). - Copy the
whsec_…secret from#webhook-secret-onceimmediately (Copy puts it on your clipboard). Press I’ve stored it and it is gone. Storage keeps only an encrypted copy. The secret is never included in the JSON body we POST.
The topic list is the table in §11.1
topics. The form default on Configuration is
invoice.issued, because that is the everyday finance
doorbell and it is wired today.
A URL that already has a webhook is refused (This URL already has a webhook. Change its topics in the list below.). If that webhook was deleted, the message says to restore it instead. If secrets were never provisioned, Create webhook flashes Webhook secrets have not been provisioned for this environment.
Each webhook is a row: endpoint, topic chips, a status chip (Active, Paused, Paused after failures or Deleted) and the last delivery. The row’s round buttons:
| Button | What it does | Asks first? |
|---|---|---|
| Edit topics | Opens the row with the topic checkboxes; Save topics keeps them | No |
| Pause deliveries / Resume deliveries | Stops new deliveries, or starts them again and clears the failure count | No — undo is one click |
| Rotate secret | Mints a new whsec_ and shows it once, like Create |
Yes: the row explains the 24-hour overlap |
| Delete webhook | Stops deliveries at once. The row stays as Deleted, with its delivery history | Yes |
| Restore webhook | Brings a deleted webhook back as Active | No |
After a rotation, deliveries are signed with both the new and the old secret for 24 hours, so the far side can switch without missing a notice (Market API rule — V13 §13). The row says Old secret valid until … while that window is open. Rotating again inside the 24 hours ends the older secret at once. A topic a webhook holds but can never receive at its scope shows as an amber chip marked (not sent here).
What “done” looks like on the far side
We POST JSON to your URL with Standard Webhooks headers. A reply in
200–299 completes the delivery
(DELIVERED). Timeouts, 408 / 425 / 429,
server errors (500+), and network failures are
TRANSIENT — we retry. Any other reply, including
410 Gone, is REFUSED — no retry; the
delivery is dead at once.
flowchart TB
A[You create a webhook and copy the whsec_ secret] --> B[An event of that topic happens]
B --> C[We POST a signed JSON body to your HTTPS URL]
C --> D{What did the door say?}
D -->|200-299| E[DELIVERED — consecutive failures reset]
D -->|timeout, 408, 425, 429, 500+| F[RETRYING — wait, then try again]
D -->|other, including 410| G[DEAD at once]
F --> H{Eight attempts used up?}
H -->|no| C
H -->|yes| I[DEAD — a dead letter]
I --> J[Five dead letters in a row pause the subscription]
Retry gaps (then a small deterministic jitter, about ±10 %):
5 seconds → 5 minutes → 30 minutes → 2 hours → 5 hours → 10 hours → 24 hours.
Eight unsuccessful attempts exhaust a schedule. A retry left waiting
more than 72 hours is swept to dead
(DeadLetterSweep, 00:20 UTC) with the error Retry
schedule abandoned for 72 hours.
Pause stops new events. In-flight deliveries keep their schedule (a paused subscription snoozes 300 seconds and tries again). Five consecutive dead letters pause automatically. Resume is always a human click — the toggle also clears the consecutive-failure counter. DISABLED kills in-flight work as dead (Subscription disabled).
The operations page
(/ops/events)
Same gate as the Feed registry: :sim_enabled, no seat,
type the URL or follow Delivered events →.
Follow along. Any dev/demo build. Path:
/ops/feeds→ Delivered events →. You should see: Subscriptions, Delivery log and dead letters, and an empty Attempt timeline until you press Timeline.
| Block | What you see | What you can press |
|---|---|---|
| Subscriptions | Callback URL; scope_kind · status · topics |
Pause / Resume
(#toggle-{id}) |
| Delivery log | Event id, status (PENDING / RETRYING /
DELIVERED / DEAD), attempt count, last HTTP /
error |
Timeline on any row; Retry only
when status is DEAD |
| Attempt timeline | Attempt number, time, outcome, HTTP or — |
Fills when you pick Timeline; — means no HTTP response
(timeout / network) |
Retry re-enqueues the dead letter and keeps the old attempt rows. You get a new schedule; you do not rewrite history.
On Configuration, Recent deliveries lists the latest 20 deliveries across your webhooks: topic, endpoint, a status chip (Queued, Retrying, Delivered or Failed), attempts and time. A Failed row has Send again, which is the same Retry.
flowchart LR
A[Delivery is DEAD] --> B[You press Retry]
B --> C[New attempt schedule starts]
C --> D[Previous Timeline rows stay]
Local receiver and the production key
Dev, demo and test deliver to an in-process receiver
(WB.Events.Sink). It checks the signature on the exact
bytes. Host failing-sink.example answers
503 on purpose so you can watch retries and dead
letters without a real outage.
Production reads EVENTS_SECRET_KEY from
deployment secrets (a base64-encoded 32-byte key). Provision it
before anyone creates a subscription, and keep it
across restarts. Losing it makes stored whsec_ blobs
unreadable — you cannot verify or rotate until a new key is in place,
and old secrets stay dark.
Careful. Copy
whsec_when you see it. Do not putEVENTS_SECRET_KEYor anywhsec_in this manual, in a ticket, or in a screenshot.
Where this is taught. How a machine verifies
webhook-signature, the 20 KB payload cap, and/subscriptions/:id/deliveries/:delivery_id/retryare V13 §13. This chapter owns the human screens and the dead-letter story.
11.2 Plant telemetry feeds
A plant feed is a TELEMETRY row for one generator
site, one provider, and one effective start date. Registering it on the
owner’s plant screen creates the operations identity
plant:{site-id}:{provider}:{from} and stores a hashed
draft data schedule. Signing that schedule is a
separate commercial act.
| Kind | What you are pointing at | Mode |
|---|---|---|
| mock | Generated demonstration readings | MOCK |
| push_api | The plant’s collector POSTs to the platform | LIVE |
| gateway_mirror | A file dropped in the operator inbox (historian CSV) | LIVE |
| vendor_cloud | Huawei / Sungrow / SMA / SolarEdge | Reserved — cannot be activated; the milestone is not opened |
A commercial vendor row still needs a credential reference even while unarmed. Kind must match provider for mock / push / mirror.
Successful batches record accepted and rejected counts. The raw payload artifact is the bytes that arrived. Sample revisions keep a correction’s provenance — they never silently rewrite the first truth. Stale and recovered transitions update the feed’s operational status (the plant freshness sweep every five minutes, distinct from the registry’s fifteen-minute sweep).
These feeds are not settlement meters and not carbon inverter MRV. The plant screen links the feed, the draft schedule, and the immutable curtailment traces.
flowchart TB
A[Owner registers mock, push, or mirror] --> B[TELEMETRY row appears on /ops/feeds and Your feeds]
B --> C[Collector pushes or the inbox sweep reads a file]
C --> D[Accepted and rejected counts; raw bytes stored]
D --> E[Plant now panel can show a fresh sample]
E --> F[Settlement still reads the 20-minute METER rows]
Follow along. Seat:
ipp_2@webill.net(Organization; passwordsupersecret). Path: Atlantis Wind → Strategy → Plant (or Dashboard Plant now → Open plant). You should see: path, latest observation time, and a mock badge on demonstration readings.
Where this is taught. How the owner registers, consents, and reads the panel: V4 §11 and V2 §11. The push API and mirror CSV schema: V13 §17. This chapter owns the registry row and the “not a settlement meter” boundary.
11.3 What runs while you sleep
You do not press these. They keep the pipes honest.
| When (UTC) | Worker | What you notice |
|---|---|---|
| Every 15 minutes | Feeds.Workers.FreshnessSweep |
Badges refresh; a new STALE / REFUSING finding if the change just happened |
| 01:10 | Feeds.Workers.AttemptRetention |
Attempts older than 90 days drop; the latest attempt on each feed stays |
| 00:20 | Events.Workers.DeadLetterSweep |
Retries abandoned for 72 hours become DEAD |
| On each event | Events.Workers.DeliverWebhook |
Up to eight signed POSTs on the events queue |
| Every minute | Events.Workers.DispatchPending |
Recovers a committed outbox row if the original transaction did not notify |
A rolled-back source transaction leaves no delivery. These jobs do not change the monthly settlement-chain order.
Where this is taught. The full clock, including the plant telemetry workers (mirror inbox, plant freshness, MEC scan, traces), is §3.
