Wiring a billing portal to a game panel: the WiseCP to AMP order pipeline

jun 17 2026 · 4 min read

The game server platform runs two separate commercial products: WiseCP handles the billing storefront and self-service portal, and AMP (Application Management Panel by CubeCoders) handles game instance lifecycle. Neither product knows the other exists. Before this integration, the workflow was manual: someone would place an order, I would read the order notification, log into the panel, create the instance, EULA-accept it, start it, and open the firewall port. That chain is error-prone, does not scale even to a handful of friends, and has no cancellation path.

The goal was to eliminate all of that. An order comes in, a playable server starts, and the port is open. A cancellation comes in, the server stops, and the port closes. No manual steps anywhere in the path.

The module design

WiseCP supports custom modules written in PHP. A module receives lifecycle hooks from the billing system: create, suspend, unsuspend, terminate, and a few others. The module I wrote handles each hook by calling the AMP controller API.

The create hook does four things in sequence: authenticate to the AMP controller, call the instance creation endpoint, call the EULA-accept endpoint (AMP refuses to start an instance that has not been EULA-accepted), and then start the instance. The AMP instance ID that comes back from the creation call is stored in WiseCP's module_data field on the order. That stored ID is the permanent link between the billing record and the game panel instance for every subsequent lifecycle call.

The terminate hook runs the same sequence in reverse: stop the instance, deallocate it. The port-forward rule tied to that instance is removed at the same time.

Auth model complexity

AMP's auth model deserves its own paragraph because it is not obvious from the documentation. The controller (the Application Deployment Server, or ADS) is the top-level management process. Game instances run under the ADS and are accessed through it via proxy-login. You authenticate to the ADS first, then call per-instance endpoints through that authenticated session, which inherits the ADS credentials.

This means the module does not store per-instance credentials anywhere. It holds one set of controller credentials, authenticates once per request, and proxies all instance-level calls through that session. The linking key is the instance ID in module_data, not any stored credential for the instance itself.

Getting this right required reading the AMP source and experimenting with the API, because the proxy-login sequence is not fully documented in one place. The first attempt authenticated to the controller but tried to call instance endpoints with a fresh session, which failed. The correct sequence is: authenticate to ADS, use the returned session to proxy-login to the specific instance, then make the instance call within that proxied context.

The cancellation path

When I started the module I built the provisioning path first and planned to add cancellation later. That was a mistake in reasoning even if not in outcome. The cancellation path exercises different code branches, different error conditions (what if the instance is already stopped, what if it was never started, what if the AMP controller is unreachable), and the firewall rule removal is a separate API call that can fail independently. I ended up writing more defensive code around the termination hook than the creation hook because the failure modes are more varied.

The lesson I took from this is that automation glue between billing and infrastructure is load-bearing in a way that greenfield code is not. If the provisioning path fails, the user has a failed order they can see and retry. If the termination path fails silently, a port stays open and an instance keeps running with no billing attached. The blast radius of a silent cancellation failure is higher than a provisioning failure, so it needed the same test coverage.

Finding along the way

While building the module I noticed that WiseCP's contact form was silently dropping every submission. The form was posting to a handler that expected a specific schema and the schema did not match the form's field names. Submissions were accepted with a 200 response and then discarded. I fixed the schema mismatch as part of the same work.

This kind of finding is common when you read code you did not write. The contact form and the game panel module share nothing structurally, but the same codebase contained both bugs, and they both had the same symptom: things appeared to work and then nothing happened.

Current state

The full lifecycle is live. An order through the storefront creates a running, playable instance. Cancellation tears it down and closes the port. The AIOps layer (n8n workflows backed by Claude) handles crash detection, log analysis, and automated remediation for the supported game types, sitting on top of the same AMP API the module uses for provisioning.

The integration covers Minecraft, Space Engineers, Satisfactory, and Eco. Each game type has its own startup and shutdown behavior in AMP, but the module does not need to know about that. AMP abstracts the game-specific lifecycle; the module only calls the generic start and stop endpoints.