# 외부 지갑

> 전용 Nodaro Cloud 배포 환경을 내 공유 지갑에 연결하세요. 지갑은 내 모든 제품에서 각 고객의 크레딧을 예약하고, 정산하고, 보고합니다.

Source: https://nodaro.ai/ko/docs/developers/external-wallet

**외부 지갑**은 각 고객의 공유 예산을 보관하는, 내가 운영하는 서비스입니다. 전용 Nodaro Cloud 배포 환경은 지출하기 전에 이 서비스에 확인합니다. Nodaro는 생성할 때마다 먼저 지갑에 크레딧 예약을 요청하고, 그 후 정확한 금액을 정산하고, 잔액을 읽어 고객에게 보여 줍니다. 플랫폼 사용료는 여전히 배포 환경의 결제 계정이 선불 풀에서 냅니다. 지갑은 내 모든 제품에 걸쳐 각 고객이 그중 얼마를 쓸 수 있는지 결정합니다.

## 작동 방식
- **결제 계정이 비용을 냅니다.** 배포 환경은 하나의 결제 계정(서피스 프로필의 `billing.payerAccount`)을 지정하며, 그 선불 풀이 배포 환경의 모든 사용 비용을 냅니다. [에디션과 서피스 프로필](https://nodaro.ai/docs/self-hosting/editions-and-profiles)을 참조하세요.
- **SSO가 고객을 식별합니다.** 고객은 배포 환경의 [SSO 공급자](https://nodaro.ai/docs/developers/sso)로 로그인합니다. 공급자가 고객에게 부여한 서브젝트는 `sso_subject`로 전송되며, 내 지갑은 이 값으로 고객을 알아봅니다.
- **지갑이 결정합니다.** 모든 단일 노드 요청과 모든 워크플로 노드에 대해, Nodaro는 결제 계정의 선불 풀을 확인한 다음, 내 지갑에 고객의 공유 예산에서 예약하도록 요청합니다. 고객의 로컬 Nodaro 크레딧 잔액은 아무 역할도 하지 않습니다.

선불 풀이 비어 있으면 지갑에 묻기 전에 요청이 중단될 수 있습니다. 지갑에 물은 뒤에는, 고객의 공유 예산에 대한 지갑의 결정이 최종 결정입니다.

### 예약
고객이 생성을 시작합니다. Nodaro가 `operation_id`와 금액을 담아 `reserve`를 보내면, 내 지갑은 그 금액을 원자적으로 예약하고 `allow` 또는 `deny`로 응답합니다.

### 실행
`allow`이면 모델이 실행됩니다. 그 밖의 응답이면 모델에 아무것도 전송되지 않습니다.

### 정산
작업이 끝나면 Nodaro가 실제 금액을 담아 `settle`을 보냅니다. 내 지갑은 정확히 그 금액을 청구하고 나머지 예약을 해제합니다.

### 잔액 표시
스튜디오가 내 지갑에 고객의 `balance`를 요청하고, 배포 환경의 표시 단위로 보여 줍니다.

## 배포 환경 구성
API와 워커에 다음 변수를 설정하세요.

| 변수 | 설정하는 내용 |
| --- | --- |
| `DEPLOYMENT_WALLET_URL` | 지갑의 엔드포인트입니다. HTTPS를 사용해야 합니다. |
| `DEPLOYMENT_WALLET_TOKEN` | Nodaro가 Bearer 토큰으로 보내는 전용 서버 비밀 값입니다. |
| `DEPLOYMENT_WALLET_SSO_PROVIDER` | 고객을 식별하는 서브젝트를 발급하는 SSO 공급자의 ID입니다. |
| `DEPLOYMENT_WALLET_TIMEOUT_MS` | 각 지갑 호출의 시간 제한으로, 1000~30000밀리초입니다. 기본값은 10000입니다. |

- **처음 세 개는 함께 설정하세요.** 배포 환경의 기존 결제 방식을 유지하려면 세 개 모두 설정하지 마세요.
- **로컬 허용량을 끄세요.** 사용자별 허용량 적용인 `billing.allowances: "enforce"`는 꺼져 있어야 합니다. 둘을 함께 쓰는 구성은 활성화할 때 거부됩니다.
- **먼저 단위를 합의하세요.** 지갑을 활성화하기 전에 Nodaro 크레딧과 내 제품 예산 사이의 환산 방식을 정하세요. 두 제품이 같은 수치를 보여 주도록 서피스 프로필의 `billing.unitRate`와 `billing.unitLabel`을 그에 맞게 설정하세요.

## 요청 계약
Nodaro는 모든 호출을 `POST`로 보내며, JSON 본문에는 `contract: 1`과 `unit: "nodaro_credit"`이, 헤더에는 `Authorization: Bearer <token>`이 들어 있습니다. 금액은 Nodaro 크레딧입니다. 내 지갑이 직접 관리하는 명시적인 환산 없이 이 금액을 내 제품의 단위로 읽지 마세요.

### 예약
```json
{
"contract": 1,
"unit": "nodaro_credit",
"action": "reserve",
"operation_id": "a3354131-4e5e-43a6-8f61-3914f09c658e",
"job_id": null,
"user_id": "5bf0d884-47b1-468e-a7b2-2433f957b267",
"sso_provider": "partner",
"sso_subject": "customer-123",
"model_identifier": "example-model",
"reserved_credits": 30
}
```

| 필드 | 의미 |
| --- | --- |
| `operation_id` | 하나의 사용 기록입니다. 작업이나 워크플로 하나에 여러 개가 있을 수 있습니다. |
| `sso_subject` | 고객입니다. Nodaro 서버가 관리하는 신뢰할 수 있는 로그인 데이터에서 오므로, 고객을 식별하는 데 사용하세요. |
| `sso_provider`, `user_id` | 요청을 보낸 SSO 공급자와 Nodaro 사용자입니다. |
| `model_identifier` | 청구 대상 모델입니다. |
| `reserved_credits` | 예약할 크레딧의 정수 값입니다. |

응답하기 전에 고객의 공유 가용 예산에서 그 금액을 원자적으로 예약하세요. 요청을 허용하려면 다음과 같이 응답하세요.

```json
{"contract":1,"unit":"nodaro_credit","operation_id":"a3354131-4e5e-43a6-8f61-3914f09c658e","decision":"allow","reserved_credits":30}
```

잔액이 부족하거나 지출할 수 없는 계정이면, 같은 엔벨로프에 `"decision": "deny"`를 담아 HTTP `200`으로 응답하세요. 금액은 필요하지 않습니다. 서비스 오류에는 `2xx` 범위 밖의 상태로 응답하세요.

내 지갑이 허용하지 않으면 모델에 아무것도 전달되지 않습니다. 시간 초과, 형식이 잘못된 응답, 금액이나 ID의 불일치, 누락된 신원 정보, 거부는 모두 요청을 중단합니다. 요청의 `Idempotency-Key` 헤더는 `<operation_id>:reserve`입니다.

### 정산 또는 해제
작업이 끝나면 Nodaro는 같은 오퍼레이션 필드에 `"action": "settle"`과 `actual_credits`를 담아 보냅니다. 정확히 그 금액을 청구하고 나머지 예약을 해제하세요. `0`으로 정산하면 모두 해제됩니다.

```json
{"contract":1,"unit":"nodaro_credit","operation_id":"a3354131-4e5e-43a6-8f61-3914f09c658e","settled":true,"actual_credits":12}
```

- **키는 `<operation_id>:settle`입니다.** 전달은 최소 한 번 보장 방식입니다. 재시도에는 다시 청구하지 않고 원래 결과를 반환해야 합니다.
- **충돌하는 요청은 거부하세요.** 같은 오퍼레이션에 대해 신원, 예약 상한 또는 최종 금액이 다른 요청은 거부하세요.
- **정산이 지연된 예약보다 먼저 도착할 수 있습니다.** `0` 정산은 아직 모르는 오퍼레이션이라도 영구적인 최종 기록을 만들어야 합니다. 나중에 도착한 예약이 그 기록을 다시 열어서는 안 됩니다.
- **예약은 만료되지 않습니다.** 생성과 그 결과에 대한 사람의 검토는 어떤 HTTP 요청보다도 오래 걸릴 수 있습니다. 각 예약은 정산될 때까지 유지하고, 멱등성 기록도 보관하세요. 콜백이 늦는다는 이유로 활성 예약을 해제하지 마세요.

### 잔액
```json
{"contract":1,"unit":"nodaro_credit","action":"balance","user_id":"5bf0d884-47b1-468e-a7b2-2433f957b267","sso_provider":"partner","sso_subject":"customer-123"}
```

```json
{"contract":1,"unit":"nodaro_credit","sso_subject":"customer-123","available_credits":250}
```

- **가용 금액은 예약을 뺀 금액입니다.** `available_credits`는 내 모든 제품에 걸친 미정산 예약을 모두 제외합니다.
- **여기서는 소수를 쓸 수 있습니다.** 잔액은 소수일 수 있으므로, 청구 가능한 1크레딧보다 작은 나머지도 표시할 수 있습니다. 예약 금액과 정산 금액은 항상 정수 크레딧입니다.
- **스튜디오에는 하나의 수치만 표시됩니다.** 스튜디오는 잔액을 배포 환경의 표시 단위로 한 번 환산하고, 자체 사용자별 허용량 표시는 숨깁니다. 잔액을 가져올 수 없으면 알 수 없음으로 표시됩니다.
- **동시 요청은 예약이 결정합니다.** 요청이 같은 순간에 도착하면, 잔액이 아니라 예약 호출이 기준이 됩니다.

## 전달과 복구
- Nodaro는 각 정산을 즉시 전달하려고 합니다. 실패하면 1분마다 실행되는 워커가 최대 1시간의 백오프로 다시 시도합니다. 확인되지 않은 정산은 전달될 때까지 보관됩니다.
- Nodaro가 스스로 취소하는 것은 한 번도 승인되지 않은 예약뿐입니다. 승인되어 실행 중인 작업은 일반적인 작업 복구 절차를 따릅니다.
- **오퍼레이션이 열려 있는 동안에는 지갑 구성을 제거하거나 다른 지갑을 가리키도록 바꾸지 마세요.** 토큰을 교체하려면 같은 지갑에서 교체하세요.

## 가격 조회
`GET /v1/deployment-billing/pricing`은 내 지갑 자체의 표시와 계획에 쓸 수 있도록 배포 환경의 가격표를 반환합니다. 결제 계정의 세션이나 그 계정의 결제 연동 키 중 하나가 필요합니다.

| 필드 | 의미 |
| --- | --- |
| `creditCost` | 모델의 기본 요금입니다. |
| `creditIdentifier` | 청구되는 식별자입니다. |
| `pricingBasis` | 기본 요금이 전제하는 조건입니다. |
| `pricing` | 텍스트 모델의 경우 각 오퍼레이션의 식별자입니다. 텍스트 모델은 기본 설정의 `llm-chat`으로 나열됩니다. |
| `denomination` | 크레딧 단위와 표시용 환산입니다. |
| `purchaseOptions` | USD로 표시한 표준 충전 금액과 크레딧당 실제 가격입니다. 별도의 상업 조건이 적용될 수 있습니다. |

- 미디어 변형과 텍스트 오퍼레이션은 현재 적용 중인 가격으로 결정됩니다. 가격이 구성되지 않은 변형은 오류 코드와 함께 `null`로 남습니다.
- 설정, 수량, 길이, 측정된 사용량에 따라 최종 청구 금액이 달라질 수 있습니다. 회계에는 예약 금액과 정산 금액을 사용하고, 이 목록의 기본 요금은 사용하지 마세요.
- 변경을 감지하려면 `If-None-Match`를 사용한 전체 요청을 권장합니다. `since` 매개변수는 가격 행의 타임스탬프를 따르므로, 런타임 구성의 모든 변경을 감지할 수는 없습니다.

## 자주 묻는 질문

### Nodaro의 외부 지갑이란 무엇인가요?

내 모든 제품에 걸쳐 각 고객의 공유 예산을 보관하는, 내가 운영하는 서비스입니다. 전용 Nodaro Cloud 배포 환경은 생성을 실행하기 전에 이 서비스에 크레딧 예약을 요청하고, 그 후 실제 금액을 정산하고, 잔액을 읽어 표시합니다.

### 어떤 Nodaro 배포 환경에서 외부 지갑을 사용할 수 있나요?

결제 계정이 있는 전용 Nodaro Cloud 배포 환경입니다. 결제 계정은 그 배포 환경의 모든 사용 비용을 내는 단 하나의 계정입니다. 고객은 배포 환경의 SSO 공급자로 로그인하며, 이 공급자가 지갑에 고객을 식별해 줍니다.

### 지갑이 제시간에 응답하지 않으면 어떻게 되나요?

아무것도 생성되지 않습니다. 시간 초과, 형식이 잘못된 응답, 금액이나 ID의 불일치, 누락된 신원 정보, 거부는 모두 모델이 실행되기 전에 요청을 중단합니다.

### 같은 정산 요청이 두 번 도착할 수 있나요?

네. 전달은 최소 한 번 보장 방식이므로, 내 지갑은 반복된 정산에 다시 청구하지 않고 원래 결과를 반환해야 합니다. 반복 요청은 Idempotency-Key 헤더로 식별합니다.

### 예약은 만료되나요?

아니요. 생성과 사람의 검토는 어떤 HTTP 요청보다도 오래 걸릴 수 있으므로, 예약은 Nodaro가 정산할 때까지 유지됩니다. 콜백이 늦는다는 이유로 활성 예약을 해제하지 마세요.
