npm package: @staratlas/profile-subscription
Description: TypeScript client for the **Profile Subscription** Solana program — fully on-chain recurring payments using Star Atlas Player Profile vaults.
Latest version: 0.46.1

# @staratlas/profile-subscription

TypeScript client for the **Profile Subscription** Solana program — fully on-chain recurring payments using Star Atlas Player Profile vaults.

| Network | Program ID |
|---------|------------|
| **Mainnet** | `PSubzJGRCQKoBadKLcg3cqryrb7ZLPujdqSokUoQptQ` |
| **Devnet / Testnet** | `PSubAEkp16MhpP7AfB5kBbpWQ4cppUj9nxS5vUkHT13` |

> **Note:** The SDK's exported `PROFILE_SUBSCRIPTION_PROGRAM_ADDRESS` constant is the devnet address. For mainnet deployments, pass the mainnet program ID explicitly via the `programAddress` config option on each instruction builder.

## Install

```bash
npm install @staratlas/profile-subscription @solana/kit
```

## Quick Start

```typescript
import {
  getInitializeSubscriptionInstruction,
  getSettleInstruction,
  getCancelSubscriptionInstruction,
  getUpdatePayoutKeyInstruction,
  getCloseSubscriptionInstruction,
  fetchSubscriptionAccount,
  findSubscriptionAccountPda,
  PROFILE_SUBSCRIPTION_PROGRAM_ADDRESS,
} from '@staratlas/profile-subscription';
```

---

## Subscription Lifecycle

```
┌──────────┐   subscribe   ┌────────┐   settle (success)   ┌────────┐
│          │ ──────────────▶│ Active │ ────────────────────▶ │ Active │
│  (none)  │                └────────┘                       └────────┘
└──────────┘                    │  │                              │
                                │  │ settle (transfer fails)     │
                                │  ▼                             │
                                │ ┌──────────┐                   │
                                │ │ PastDue  │───settle (ok)────▶│
                                │ └──────────┘                   │
                                │      │                         │
                                │      │ cancel                  │ cancel
                                │      ▼                         ▼
                                │ ┌───────────┐            ┌───────────┐
                                └▶│ Cancelled │            │ Cancelled │
                                  └───────────┘            └───────────┘
                                       │ settle (up to cancelled_at)
                                       ▼
                                  ┌──────────┐
                                  │ Expired  │──── close ────▶ (account deleted)
                                  └──────────┘
```

### Status Definitions

| Status | Meaning | Can Settle? | Can Cancel? | Can Close? |
|--------|---------|-------------|-------------|------------|
| **Active** | Subscription is live, payments current | ✅ | ✅ | ❌ |
| **PastDue** | Settlement attempted but transfer failed (insufficient funds, revoked delegate, etc.) | ✅ | ✅ | ❌ |
| **Cancelled** | Subscriber cancelled. Outstanding periods before `cancelled_at` can still be settled | ✅ (up to boundary) | ❌ | ❌ |
| **Expired** | Cancelled + fully settled. Terminal state | ❌ | ❌ | ✅ |

---

## Instructions

### 1. Initialize Subscription

Creates a new subscription. The subscriber signs with their Player Profile key (requires `SUBSCRIBE` permission scoped to the subscription program).

```typescript
import { findSubscriptionAccountPda, getInitializeSubscriptionInstruction } from '@staratlas/profile-subscription';

// Derive the subscription PDA
const [subscriptionPda] = await findSubscriptionAccountPda({
  profile: profileAddress,
  serviceProvider: serviceProviderAddress,
  subscriptionId: 1n, // unique per profile+provider
});

const ix = getInitializeSubscriptionInstruction({
  // Profile validation accounts
  profileValidationSigner: subscriberKeypair,
  profileValidationProfile: profileAddress,
  profileValidationCertificate: undefined, // optional
  profileValidationProgram: PLAYER_PROFILE_PROGRAM_ADDRESS,
  // Subscription accounts
  subscriptionAccount: subscriptionPda,
  serviceProvider: serviceProviderAddress,
  tokenMint: usdcMintAddress,
  systemProgram: SYSTEM_PROGRAM_ADDRESS,
  profileProgram: PLAYER_PROFILE_PROGRAM_ADDRESS,
  // Instruction args
  keyIndex: 0,         // profile key index with SUBSCRIBE permission
  subscriptionId: 1n,  // unique subscription identifier
  amountPerPeriod: 29_990_000n,  // 29.99 USDC (6 decimals)
  periodSeconds: 2_592_000n,     // 30 days
  metadata: new Uint8Array(64),  // custom tier/plan metadata
});
```

**After initialization**, the subscriber must also approve the subscription PDA as a token delegate on their token account:

```typescript
// Client-side: approve delegate for the subscription PDA
const approveIx = createApproveInstruction(
  subscriberTokenAccount,
  subscriptionPda,      // delegate
  subscriberWallet,     // owner
  BigInt(2**64 - 1),   // max approval
);
```

### 2. Settle (Permissionless Crank)

Collects payment for elapsed billing periods. **Anyone can call this** — no signer required beyond the transaction fee payer.

```typescript
import { getSettleInstruction } from '@staratlas/profile-subscription';

const ix = getSettleInstruction({
  subscriptionAccount: subscriptionPda,
  sourceTokenAccount: subscriberUsdcAta,       // subscriber's token account
  destinationTokenAccount: providerUsdcAta,     // service provider's token account
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
```

**Settlement rules:**
- Calculates `periods_owed = (now - last_settled_at) / period_seconds`
- **Capped at 3 periods per transaction** (prevents accumulation shock)
- Transfers `periods_owed × amount_per_period` tokens
- Advances `last_settled_at` by exact period increments (no drift)
- If the transfer fails (insufficient funds, revoked delegate), sets status to `PastDue`

**For cancelled subscriptions:** only settles up to `cancelled_at` timestamp, then transitions to `Expired`.

### 3. Cancel Subscription

Subscriber cancels. Requires `CANCEL_SUBSCRIPTION` permission on their profile key.

```typescript
import { getCancelSubscriptionInstruction } from '@staratlas/profile-subscription';

const ix = getCancelSubscriptionInstruction({
  profileValidationSigner: subscriberKeypair,
  profileValidationProfile: profileAddress,
  profileValidationCertificate: undefined,
  profileValidationProgram: PLAYER_PROFILE_PROGRAM_ADDRESS,
  subscriptionAccount: subscriptionPda,
  profileProgram: PLAYER_PROFILE_PROGRAM_ADDRESS,
  keyIndex: 0,
});
```

**Important:** Cancellation does NOT settle outstanding payments. The service provider should call `Settle` before or after cancellation to collect any owed periods up to `cancelled_at`.

### 4. Update Payout Key

Service provider changes the payout destination. Only the current `payout_key_owner` can call this.

```typescript
import { getUpdatePayoutKeyInstruction } from '@staratlas/profile-subscription';

const ix = getUpdatePayoutKeyInstruction({
  payoutKeyOwner: currentServiceProviderKeypair,
  subscriptionAccount: subscriptionPda,
  newServiceProvider: newPayoutAddress,
});
```

**Note:** This updates both `service_provider` (payout destination) AND `payout_key_owner` (signing authority). The old owner loses control permanently.

### 5. Close Subscription

Reclaims rent from a terminal (Expired) subscription account.

```typescript
import { getCloseSubscriptionInstruction } from '@staratlas/profile-subscription';

const ix = getCloseSubscriptionInstruction({
  rentRecipient: subscriberKeypair,   // receives the rent refund
  subscriptionAccount: subscriptionPda,
});
```

Only works on `Expired` subscriptions.

---

## Reading Subscription State

```typescript
import { fetchSubscriptionAccount } from '@staratlas/profile-subscription';

const sub = await fetchSubscriptionAccount(rpc, subscriptionPda);

console.log(sub.data.status);           // 'Active' | 'PastDue' | 'Cancelled' | 'Expired'
console.log(sub.data.amountPerPeriod);  // bigint (token base units)
console.log(sub.data.periodSeconds);    // bigint
console.log(sub.data.periodsSettled);   // bigint
console.log(sub.data.lastSettledAt);    // bigint (unix timestamp)
console.log(sub.data.cancelledAt);      // bigint (0 if not cancelled)
console.log(sub.data.serviceProvider);  // Address
console.log(sub.data.tokenMint);        // Address
console.log(sub.data.metadata);         // Uint8Array(64)
```

---

## Service Provider Integration Guide

### What Your System Needs to Do

As a service provider integrating profile subscriptions (e.g., Zink), your backend is responsible for:

#### 1. Transaction Construction (Frontend/API)

When a user clicks "Pay with Crypto":
- Derive the subscription PDA from their profile + your service provider address + a unique subscription ID
- Build the `InitializeSubscription` transaction with your configured parameters (amount, period, metadata)
- Include the SPL token `approve` instruction for the subscription PDA delegate
- Present for the user to sign

**Your liability:** The `service_provider` address, `amount_per_period`, `period_seconds`, and `metadata` are set by the transaction you construct. Triple-check these values — they're immutable after creation.

#### 2. Settlement Crank (Backend)

Run a bot/cron that periodically calls `Settle` on all active subscriptions:

```typescript
// Poll all subscription PDAs for your service provider
// For each active subscription where time > last_settled_at + period_seconds:
const ix = getSettleInstruction({
  subscriptionAccount: pda,
  sourceTokenAccount: subscriberAta,
  destinationTokenAccount: yourAta,
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
// Send transaction (any wallet can pay the fee)
```

**Settlement is capped at 3 periods per transaction.** If a subscriber was PastDue for months, you need to call settle multiple times to collect all owed periods.

**Your liability:** If you don't run the crank, you don't get paid. The program doesn't push payments — it requires an explicit settle call.

#### 3. Status Sync (Backend)

Monitor subscription account changes and sync to your off-chain database:

| On-chain Status | Your System Should |
|---|---|
| **Active** | Grant access. Verify `periodsSettled` is current. |
| **PastDue** | Grace period → attempt settle again. After N failures, restrict access. |
| **Cancelled** | Settle any remaining periods. Access continues until end of current paid period. |
| **Expired** | Revoke access. Optionally close the account to reclaim rent. |

#### 4. Metadata Usage

The 64-byte `metadata` field is yours to define. Example encoding:

```
bytes 0-3:   plan_id (u32)        — which subscription tier
bytes 4-7:   features (u32)       — feature flags bitmask  
bytes 8-15:  custom_id (u64)      — your internal reference ID
bytes 16-63: reserved             — future use
```

This is set at initialization and is immutable. If a user changes plans, create a new subscription (cancel old → init new).

### Security Considerations for Service Providers

1. **Validate the subscription PDA derivation** — When your backend reads subscription data, verify the PDA is correctly derived from the expected seeds. Don't trust arbitrary account addresses.

2. **Verify your service_provider address** — The `destination_token_account` owner in Settle must match `service_provider` on the subscription. The program enforces this, but your off-chain system should also verify it matches your expected wallet.

3. **Handle PastDue gracefully** — A PastDue status means the token transfer failed. This could be:
   - Subscriber ran out of funds
   - Subscriber revoked the token delegate
   - Subscriber closed their token account
   
   Your system should attempt to re-settle periodically but eventually give up and restrict access.

4. **Don't rely solely on on-chain state for access control** — Use on-chain state as the source of truth, but cache locally with reasonable TTLs. Polling every subscription every second is wasteful; every few minutes is fine for most use cases.

5. **Payout key security** — If your `payout_key_owner` private key is compromised, an attacker can redirect all future settlements to their wallet via `UpdatePayoutKey`. Store this key with the same security as your hot wallet.

6. **Settlement timing** — The program uses `Clock::get()` for timestamps. Validators have ~1-2 second clock skew tolerance. Don't rely on sub-second timing precision for billing period boundaries.

---

## Permissions

The program uses Player Profile permissions scoped to `PSubAEkp16MhpP7AfB5kBbpWQ4cppUj9nxS5vUkHT13`:

| Permission | Bit | Required For |
|---|---|---|
| `SUBSCRIBE` | `1 << 0` | `InitializeSubscription` |
| `CANCEL_SUBSCRIPTION` | `1 << 1` | `CancelSubscription` |

Add these to a profile key:
```typescript
const permissions = (1n << 0n) | (1n << 1n); // SUBSCRIBE + CANCEL_SUBSCRIPTION
// Use AddKeys instruction on the Player Profile program
// with scope = PROFILE_SUBSCRIPTION_PROGRAM_ADDRESS
```

---

## Program Addresses

| Network | Program ID |
|---------|------------|
| **Mainnet** | `PSubzJGRCQKoBadKLcg3cqryrb7ZLPujdqSokUoQptQ` |
| **Devnet / Testnet** | `PSubAEkp16MhpP7AfB5kBbpWQ4cppUj9nxS5vUkHT13` |

### Related Programs

| Program | Mainnet Address |
|---------|----------------|
| Player Profile | `pprofELXjL5Kck7Jn5hCpwAL82DpTkSYBENzahVtbc9` |
| Profile Vault | `pv1ttom8tbyh83C1AVh6QH2naGRdVQUVt3HY1Yst5sv` |
| SPL Token | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` |

### Constants

| Constant | Value | Description |
|---|---|---|
| `MAX_SETTLEMENT_PERIODS` | 3 | Max periods settled per transaction |
| `PROFILE_SUBSCRIPTION_PROGRAM_ADDRESS` | `PSubAEkp16MhpP7AfB5kBbpWQ4cppUj9nxS5vUkHT13` | SDK default (devnet) |

### Using the Mainnet Program ID

The SDK defaults to the devnet program address. For mainnet, override via the `config` parameter:

```typescript
const MAINNET_PROGRAM_ID = 'PSubzJGRCQKoBadKLcg3cqryrb7ZLPujdqSokUoQptQ';

const ix = getInitializeSubscriptionInstruction(
  { /* ...accounts and args... */ },
  { programAddress: address(MAINNET_PROGRAM_ID) }
);
```

---

## License

Apache-2.0