Raw .md
Shared Wallet & Ledger Shared Wallet Updated Sep 23, 2026

Architectural specification for the centralized ledger balance synced across ternisdomains.de, thosted.de, and Ternis Auth applications.

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:

  1. Strict Idempotency: Each charge request requires or accepts an idempotency_key. If network timeouts cause ternisdomains.de to retry a purchase request, the platform returns the previous transaction rather than charging the user twice.
  2. 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.
  3. Audit Trail: Every transaction records source_service, reference_id (e.g. domain name acme.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();
    }
}