> ## Documentation Index
> Fetch the complete documentation index at: https://docs.musterbox.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Secure purchases

> The SDK's purchase bridge — environment pre-flight, PIN mint, and single-shot verification.

Buying an item for real value is the highest-stakes flow in the platform. It
is **SDK-mediated by design**: the SDK owns the environment pre-flight, the
bearer authentication, the backend paths, and the response parsing. Your game
only provides business data.

<Warning>Never hand-roll a purchase. Route all purchases through the SDK's `PurchaseClient` — the SDK core is the only place that knows the purchase endpoint paths.</Warning>

## The flow

```text theme={"dark"}
 1.  PurchaseClient.mint_pin_session(MintRequest)
       │
       ├─ environment integrity check ──risks?──▶ blocked (ENV_COMPROMISED reported)
       │                                              PurchasePreflight::Blocked
       │
       └─ POST /api/v1/security/pin-session
               ─▶ PinSession { sessionId, sessionToken, expiresInMs, envLevel }
                            PurchasePreflight::Ready
 2.  Player enters PIN
 3.  PurchaseClient.verify_pin(sessionId, sessionToken, pin, VerifyRequest)
       └─ POST /api/v1/security/pin-session/{id}/verify
               ─▶ PinVerifyOutcome::Valid payload
               ─▶ PinVerifyOutcome::WrongPin { locked, attempts }
               ─▶ PinVerifyOutcome::Failed { code, message }
```

## Mint a PIN session

```rust theme={"dark"}
use musterbox_sdk::purchase::{PurchaseClient, MintRequest};

let client = PurchaseClient::from_session(base_url, session)?;

let request = MintRequest::new("item-arena-bundle", 9.99, "purchase-1");
match client.mint_pin_session(&request)? {
  PurchasePreflight::Ready { session } => { /* show PIN entry */ }
  PurchasePreflight::Blocked { level, risks } => { /* surface block */ }
}
```

The on-device integrity check classifies risks:

* critical risks (`DEVICE_ROOTED`, `DEBUGGER_ATTACHED`) → `COMPROMISED`,
* any other risk (screen capture, suspicious accessibility, …) → `WARN`,
* no risks → the raw integrity level (`SAFE` by default).

Any detected risk **blocks** the purchase client-side and reports
`ENV_COMPROMISED` to `/api/v1/security/events`.

## Verify the PIN

```rust theme={"dark"}
use musterbox_sdk::purchase::VerifyRequest;

let req = VerifyRequest::new(4.5, wallet_address, "idem-key-1");
match client.verify_pin(&session.session_id, &session.session_token, pin, &req)? {
  PinVerifyOutcome::Valid { payload } => { /* authorized */ }
  PinVerifyOutcome::WrongPin { locked, attempts } => { /* retry or block */ }
  PinVerifyOutcome::Failed { code, message } => { /* session/transport error */ }
}
```

<Important>The backend consumes sessions on success and rate-limits failures. Never retry `verify_pin`.</Important>

The `amount` travels as a decimal **string** on the wire; always provide a
fresh `idempotencyKey` per authorized withdrawal.

## Security events

Best-effort audit logging (backend acknowledges with `202`). The SDK reports
automatic pre-flight blocks; use it for your own incidents:

```rust theme={"dark"}
let _ = client.report_security_event(
  "SUSPICIOUS_ACTIVITY",
  serde_json::json!({ "detail": "..." }),
)?;
```

## Wallet custody boundary

The SDK never sees, holds, or signs wallet private keys. Custody lives in
MusterBox's MPC custody layer; the player authorizes a withdrawal by PIN, and
the custody layer executes it. The `destinationAddress` your code passes to
verify is the single authorization target — validate it carefully.

Next: [Security & integrity](/sdk/security).
