A dashboard can make a complicated system feel organised while still leaving every actual decision to a person. That was becoming the next limitation of the Virya ecosystem.

CrowdRelay already knew a surprising amount: who had joined Signal, which city was heating up, how ticket inventory was moving, which merch variants were close to stockout, how campaigns performed, which shows were coming next and where a fan was in the lifecycle. The missing step was not more intelligence. It was turning that state into safe, repeatable action.

I am now turning that layer into CrowdRelay Autopilot.

The important design decision was to resist the obvious interpretation of “AI automation”. I did not want a language model with broad credentials deciding prices, spending money or sending arbitrary messages. The core had to stay boring: deterministic Rust, explicit invariants, bounded contexts and actions that can be explained after the fact.

Autonomy starts with bounded contexts

The Autopilot is therefore not an agent. It is a collection of small business domains.

Ticket Yield knows about sell-through, paid velocity, capacity, price guardrails and cooldowns. Merchandising knows about stock coverage, reorder windows and product economics. Audience Lifecycle knows whether a fan is eligible for a communication and whether a human campaign already touched that person. Booking Opportunity knows about first-party city demand, external market signals and verified outreach targets. Promotion Yield understands ROAS and budget bounds. Experimentation knows when evidence is sufficient to reallocate traffic. Show Operations knows which tasks can be proven from system state and which ones still require a human being to physically do something.

None of those domains know PostgreSQL, HTTP, n8n or Meta.

That separation matters more than the number of features.

A controller that contains SQL, API calls and business rules in one function is fast to write and expensive to trust. In ViryaOS the domain produces a typed decision. The application layer decides how that decision should pass through the current autonomy policy. Infrastructure persists immutable evidence and creates a durable action. Only the executor is allowed to touch the outside world.

A decision is not permission to mutate the world

The execution path is intentionally more conservative than the decision path.

A ticket price change records the old and proposed price. A merch price change also records the version of the economics guardrail that justified it. A booking action records the exact verified target and target version. If state changes before execution, the action is rejected instead of being “approximately correct”.

This also makes crashes less interesting.

Actions have stable idempotency keys, bounded retries and stale-processing recovery. External emissions have their own ledger. A worker can die after a side effect and restart without turning one booking follow-up into two. A blue/green deployment cannot let two workers jointly exceed an action quota. Promotion budget increases reserve their financial delta transactionally, so concurrent campaign decisions cannot quietly overrun a workspace limit.

Autonomy is a policy, not a boolean

The second design rule was that autonomy should be earned, not assumed.

Every bounded context has an authority level: observe, recommend, require approval or bounded auto. There is also a global kill switch, per-context confidence threshold and a maximum number of actions per 24 hours. Deploying the code does not turn the band into a robot. The runtime can stay completely idle until a capability is explicitly enabled.

This gives the mobile app a much better job too.

Virya Signal should not become another control panel that somebody has to babysit. The operator view is moving toward an exception cockpit: what ViryaOS did, what measurable effect followed, what is waiting for approval and what genuinely needs a human. The useful daily summary is not “there are 47 charts”. It is closer to “23 actions executed, two things need you, zero are blocked”.

Market signals are evidence, not authority

The market-intelligence layer follows the same rule of distrust.

External signals are typed observations with provenance, confidence and expiry. Streaming momentum, search interest, social momentum or live demand can influence a city opportunity, but they cannot create a booking action by themselves. First-party evidence remains dominant and the market contribution is capped. Ten duplicate signals from one category do not get ten votes.

Adaptive pricing without a magic model

The same principle applies to pricing.

There is no model hallucinating that a ticket should cost 37.42 PLN. Ticket Yield evaluates a small legal set of moves under explicit constraints. Weak demand does not automatically trigger a public discount; it can instead feed Audience Lifecycle. Merch Yield does nothing unless a product has real min/max economics configured, and any reduction must preserve the defined margin floor. The point is not to be clever. The point is to be predictably useful.

Do not confuse execution with success

The newest part is the outcome loop.

An executed action is not evidence that the decision was good. Ticket pricing is measured later against a revenue window. Merch pricing uses a gross-revenue proxy over a longer window rather than pretending unit count alone is success. Promotion waits for a full observation window before judging ROAS. Where attribution is weak, ViryaOS deliberately does not invent a learning signal.

That distinction should eventually let the heuristics calibrate from our own history without introducing an opaque machine-learning service.

PostgreSQL 18 as the durable operating substrate

PostgreSQL 18 fits this direction unusually well.

The operational data set is becoming more scan-heavy: snapshots, action ledgers, measurements, market observations and reconciliation jobs. The stack is moving to PostgreSQL 18 and its asynchronous I/O subsystem, while keeping correctness independent of any specific I/O backend. Runtime operations expose the actual database version, I/O method and concurrency settings so we can verify what production is doing instead of assuming the Compose file tells the whole story.

I am deliberately not turning this into a microservice expansion. CrowdRelay remains the kernel. PostgreSQL remains the durable state. The existing Rust worker evaluates and executes work. n8n stays useful, but as a set of hands for provider integrations rather than the place where business decisions live. Moving n8n to a separate home server gives those adapters more breathing room without moving the brain out of the Rust system.

Feature completeness should not destroy build times

The build graph matters as well.

Adding a dozen autonomous capabilities would be an easy excuse to introduce twelve crates, several new runtimes and a pile of generic abstractions. I am doing the opposite. The bounded contexts are small domain modules with cheap unit tests. Heavy I/O dependencies stay at the infrastructure edge. CI keeps its existing Rust cache strategy, and integration tests are reserved for boundaries that actually need PostgreSQL or network semantics.

The product is attention reduction

The result is starting to feel less like “software I use to run the band” and more like an operating system for the boring parts of running the band.

That is the real goal. Signal, the website, Synesthesia, ticketing and the automation stack should not create five places that require attention. They should collapse attention into a small number of decisions that cannot yet be safely automated.

The system should collect the facts, make the routine decisions, execute within explicit limits, measure what happened and come back only when it genuinely needs us.

That is a much more interesting definition of automation than adding another chatbot.

Pure policy is also a performance tool

The latest refactor kept the rule that matters most to me: the domain crates have no repository, SQL or provider knowledge. evaluate_* functions receive domain snapshots and policies and return decisions. Application code turns those results into durable candidates; infrastructure does the database work.

That separation made a small but useful optimization obvious in booking target selection. The selector used to collect eligible targets into a temporary vector and sort the whole set even though the caller needed one winner. It now performs a single deterministic pass, keeps only the current best candidate and preserves the same tie-break order. The hot path went from allocation plus O(n log n) sorting to allocation-free O(n) selection. A domain unit test locks the deterministic tie behaviour across input order.

I also added a modularity ratchet around the large Rust surfaces. The goal is not to worship line counts; localization tables and fixture-heavy integration tests are different from production orchestration. The useful rule is that production logic should remain navigable and that extracting it must not push SQL into the domain just to make a file shorter.