Shared Ecosystem Wallet & Balance Synchronization
Ternis provides a Unified Platform Credit Ledger that allows users to deposit funds once in their centralized Ternis Account and seamlessly spend those credits across ecosystem applications such as ternisdomains.de (domain registrations & renewals), thosted.de (cloud servers), and developer API calls.
1. Architectural Design & ACID Ledger
All financial movements are recorded as immutable entries in the balance_transactions table:
+-------------------------------------------------------------+
| User Account Balance |
| users.balance_cents (e.g. 5000) |
+-------------------------------------------------------------+
|
+----------------------+----------------------+
| | |
v v v
[ Top-Up / Stripe ] [ ternisdomains.de ] [ thosted.de ]
+€25.00 Deposit -€12.50 Domain Reg -€5.00 Compute
Key Guarantees:
- Strict Idempotency: Each charge request requires or accepts an
idempotency_key. If network timeouts causeternisdomains.deto retry a purchase request, the platform returns the previous transaction rather than charging the user twice. - Pessimistic Row Locking (
SELECT ... FOR UPDATE): Deductions and top-ups execute inside ACID transactions with row-level locks, eliminating race conditions when multiple microservices deduct balance simultaneously. - Audit Trail: Every transaction records
source_service,reference_id(e.g. domain nameacme.de),balance_after_cents, and metadata.
2. API Endpoints for Ecosystem Applications
2.1. Inspect Wallet Balance
Before showing checkout screens, applications can retrieve the current available balance:
GET /api/v1/user/balance HTTP/1.1
Host: auth.ternis.net
Authorization: Bearer <access_token>
Response (200 OK):
{
"user_id": "b2f6b897-4001-460d-8386-db93f1d8c1c4",
"balance_cents": 5000,
"formatted_balance": "€50.00",
"currency": "EUR",
"recent_transactions": [
{
"id": "e4f81a20-3b4c-4d5e-9f0a-1b2c3d4e5f6a",
"amount_cents": -1250,
"formatted_amount": "-€12.50",
"balance_after_cents": 5000,
"formatted_balance_after": "€50.00",
"type": "charge",
"source_service": "ternisdomains.de",
"reference_id": "domain:mycompany.de",
"description": "Domain Registration: mycompany.de (1 year) via ternisdomains.de",
"created_at": "2026-09-17T12:00:00Z"
}
]
}
2.2. Charge Balance (e.g. ternisdomains.de)
When a user confirms a domain purchase:
POST /api/v1/user/balance/charge HTTP/1.1
Host: auth.ternis.net
Authorization: Bearer <access_token>
Content-Type: application/json
{
"amount_cents": 1250,
"service": "ternisdomains.de",
"reference_id": "dom_reg_mycompany_de",
"idempotency_key": "tdom_charge_20260917_00192",
"description": "Domain registration: mycompany.de (1 year) via ternisdomains.de",
"metadata": {
"domain": "mycompany.de",
"tld": "de",
"years": 1
}
}
Success Response (200 OK):
{
"success": true,
"message": "Balance charged successfully.",
"transaction": {
"id": "9dc721a0-128f-4318-912b-31ca78198f12",
"amount_cents": -1250,
"formatted_amount": "-€12.50",
"balance_after_cents": 3750,
"formatted_balance_after": "€37.50",
"currency": "EUR",
"reference_id": "dom_reg_mycompany_de",
"source_service": "ternisdomains.de"
}
}
Insufficient Funds Response (422 Unprocessable Entity):
{
"success": false,
"message": "Insufficient balance. Required: 1250 cents, Available: 500 cents.",
"current_balance_cents": 500
}
3. Real-World Integration Blueprint for ternisdomains.de
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class TernisWalletClient
{
protected string $authBaseUrl = 'https://auth.ternis.net/api/v1';
public function purchaseDomain(string $userAccessToken, string $domainName, int $priceCents): array
{
$idempotencyKey = 'dom_order_' . md5($domainName . '_' . date('Y-m-d'));
$response = Http::withToken($userAccessToken)
->post("{$this->authBaseUrl}/user/balance/charge", [
'amount_cents' => $priceCents,
'service' => 'ternisdomains.de',
'reference_id' => $domainName,
'idempotency_key' => $idempotencyKey,
'description' => "Domain registration: {$domainName} via ternisdomains.de",
'metadata' => [
'domain' => $domainName,
'timestamp' => now()->toIso8601String(),
],
]);
if ($response->status() === 422) {
throw new InsufficientWalletBalanceException('Please top up your Ternis Wallet balance.');
}
return $response->json();
}
}