# Rentora API: Web hujjati (Documentatsiya.md)

> Bu fayl AI yordamchiga (ChatGPT, Claude va boshqalar) berish uchun yozilgan. U Rentora API'ni **sayt yoki veb-ilovada** ishlatish uchun kerak bo'lgan hamma narsani o'z ichiga oladi. Foydalanuvchining topshirig'ini shu hujjat asosida bajaring. Hujjatda yo'q endpoint, maydon yoki parametrni o'ylab topmang.

## 1. Qisqacha

- **Nima bu:** Rentora NFT'lar ro'yxatini, rasmini (`.webp`) va Lottie animatsiyasini beradigan API.
- **Bazaviy manzil:** `https://user299.hostx.uz/v1`
- **Format:** HTTPS, javoblar JSON.
- **Autentifikatsiya:** har bir so'rovda `Authorization: Bearer YOUR_API_KEY` sarlavhasi.
- **Endpointlar:** NFT ro'yxati uchun ikkita `GET`, ikkalasi ham faqat ma'lumot o'qish uchun:
  - `GET /v1/nft`: NFT'lar. Katalog admin sozlamasiga bog'liq: standart holatda Rentora katalogi **yashirin** va faqat **Market** NFT'lari qaytadi; katalog yoqilgan bo'lsa **katalog + Market** (`source=all`), `?source=catalog` faqat katalog
  - `GET /v1/nft/{id}`: bitta NFT: raqam, @username yoki NFT manzili (`EQ...`, `?address=` ham bo'ladi)
- **API kalit:** foydalanuvchi uni Rentora kabinetidan oladi.

## 2. AI uchun muhim qoidalar

1. **API kalitni hech qachon brauzerda ishlaydigan kodga (HTML, frontend JavaScript) qo'ymang.** Sahifani ochgan har kim uni ko'ra oladi. Kalit faqat server kodida turadi. Brauzer o'zining serveriga (`/api/nft`) so'rov yuboradi, server esa kalit bilan Rentora'ga murojaat qiladi. Shuning uchun saytga NFT chiqarish uchun har doim ikki qism yoziladi: server endpoint va sahifa.
2. Kalitni kodga qattiq yozish o'rniga muhit o'zgaruvchisidan olishni tavsiya qiling (masalan `RENTORA_KEY`).
3. **`image` va `lottie` manzillari ochiq**, ularni yuklash uchun kalit kerak emas.
4. Orqa fon alohida maydon emas: u Lottie animatsiyasining ichida keladi. `image` maydoni tayyor rasm.
5. Ijara uchun 4.3–4.6 bo'limlardagi endpointlar bor, NFT egasi (o'z NFT'ini ijaraga beruvchi) uchun 4.8-bo'lim. **`POST /v1/rentals` da `link` ixtiyoriy:** berilmasa NFT ulanishgacha yuborilmaydi, havola keyin `/connect` bilan keladi (ko'pi bilan 3 urinish). Narx mijozning Rentora balansidan (so'mda) yechiladi. To'lov qabul qilish yoki NFT'ni sotish uchun endpoint yo'q, uni o'ylab topmang.
6. Bazaviy manzil faqat `https://user299.hostx.uz/v1`. Boshqa domen yozmang.
7. Telegram bot uchun bu hujjat emas, `Documentatsiya-bot.md` ishlatiladi (u yerda javob shakli boshqacha: rasm yo'q, `url`, `emoji_id` va `price` bor).
8. Ro'yxatni sahifa har ochilganda Rentora'dan so'ramang. Serveringizda bir necha daqiqa saqlab (kesh) bering, aks holda `429` chiqishi mumkin.

## 3. Autentifikatsiya

Har bir so'rovda:

```
Authorization: Bearer YOUR_API_KEY
```

Kalit yo'q yoki noto'g'ri bo'lsa, API `401` qaytaradi.

### 3.1 Tezkor ma'lumotnoma

**Umumiy qoidalar**

- Bazaviy manzil: `https://user299.hostx.uz/v1`
- Kirish: `Authorization: Bearer <API kalit>` har so'rovda. Kalit yo'q yoki noto'g'ri bo'lsa `401`.
- Format: so'rov va javob JSON (UTF-8). `POST` da `Content-Type: application/json`.
- Pul: butun son, so'mda (`UZS`). Kasr yo'q.
- Xato shakli: `{"error": {"code": "...", "message": "..."}}`. Kodlar: 7-bo'lim (Xatolar).
- Limit: kalit bo'yicha kunlik limit bor. Market: `GET` daqiqada 120 (`GET /v1/nft` ning market qismi ham shu hisobga kiradi), `POST` daqiqada 10. Oshsa `429 rate_limited` va `Retry-After` (soniya). Istisno: `GET /v1/nft` da `source` berilmasa 429 emas, katalog qaytadi (4.1).
- Takrordan himoya: market `POST` larida `Idempotency-Key` (16–64 belgi) majburiy.
- `emoji_id` matn (string) bo'lib keladi. Songa aylantirmang.

**Odatiy ketma-ketlik:** `GET /v1/balance` → `GET /v1/nft` → `POST /v1/rentals` → `POST /v1/rentals/{id}/connect` → `GET /v1/rentals/{id}` yoki webhook (4.7 Webhook).

**Katalog** — NFT ro'yxati va ma'lumotnomalar. Pul yechilmaydi.

| Metod | Endpoint | Vazifasi |
|---|---|---|
| GET | `/v1/nft` | NFT'lar: market (katalog admin sozlamasi bilan yoqilsa, katalog + market); `?source=catalog` faqat katalog. Rasm, animatsiya, narx, bo'shligi |
| GET | `/v1/nft/{id}` | Bitta NFT: raqam, @username yoki manzil (`EQ...`; `?address=` ham) |
| GET | `/v1/gifts` | Telegram gift kolleksiyalari va fonlari, katalogda nechtadan borligi |
| GET | `/v1/catalog/types` | NFT turlari (username, number, gift, other) va soni |
| GET | `/v1/catalog/colors` | Gift fonlari (ranglar) |
| GET | `/v1/catalog/collections` | Gift kolleksiyalari; bittasi: /{slug} |
| GET | `/v1/catalog/links` | Barcha NFT havolalari: Telegram, sayt, Fragment |
| GET | `/v1/balance` | Balansingiz (so'm) |

**Ijara (katalogdagi NFT)** — Narx balansdan so'mda yechiladi.

| Metod | Endpoint | Vazifasi |
|---|---|---|
| POST | `/v1/rentals` | NFT'ni ijaraga olish: nft_id va days |
| GET | `/v1/rentals` | Shu kalit bilan olingan ijaralar |
| GET | `/v1/rentals/{id}` | Bitta ijara holati (status, status_message) |
| POST | `/v1/rentals/{id}/connect` | Fragment TON Connect havolasini yuborish (ko'pi bilan 3 marta) |
| POST | `/v1/rentals/{id}/extend` | Faol ijarani uzaytirish |

**Market (NFT manzili bilan)** — POST so'rovlarda Idempotency-Key sarlavhasi majburiy.

| Metod | Endpoint | Vazifasi |
|---|---|---|
| GET | `/v1/market/quote` | Narxni so'rash: address va days |
| POST | `/v1/market/orders` | Buyurtma berish (balansdan yechiladi) |
| GET | `/v1/market/orders` | Buyurtmalar ro'yxati; bittasi: /{id} |
| GET | `/v1/market/rentals` | Mening ijaralarim |
| GET | `/v1/market/rentals/{address}/extend/quote` | Uzaytirish narxi |
| POST | `/v1/market/rentals/{address}/extend` | Ijarani uzaytirish |

**NFT egasi** — O'z NFT'ingizni ijaraga berish.

| Metod | Endpoint | Vazifasi |
|---|---|---|
| GET | `/v1/owner` | Holat: hamyon, faollashtirish, daromad |
| POST | `/v1/owner/wallet` | Hamyon yaratish (24 so'z faqat shu javobda) |
| POST | `/v1/owner/activate` | Faollashtirish (0.10 TON) |
| GET | `/v1/owner/nfts` | Hamyondagi NFT'lar va ularning holati |
| POST | `/v1/owner/nfts` | Katalogga qo'shish so'rovi |
| POST | `/v1/owner/nfts/{id}` | Savdodan olish yoki qaytarish |
| GET | `/v1/owner/requests` | So'rovlar va holati |
| POST | `/v1/owner/requests/{id}/cancel` | So'rovni bekor qilish |
| GET | `/v1/owner/stats` | Qaysi NFT qancha ishladi (?days=7|30|90|0) |
| GET | `/v1/owner/earnings` | Daromad balansi va tarixi |
| GET | `/v1/owner/withdrawals` | Yechish so'rovlari |
| POST | `/v1/owner/withdrawals` | Daromadni yechish |


## 4. Endpointlar

### 4.1 `GET /v1/nft`: barcha NFT'lar

**Standart `source=all`:** `source` berilmasa javobda avval katalogdagi NFT'lar, keyin Marketdagi (katalogda yo'q) ijaraga qo'yilgan NFT'lar keladi. `?source=` bilan tanlang:

| `source` | Natija |
|---|---|
| berilmasa yoki `all` | Katalog + market. Katalog faqat 1-sahifada (`cursor` yo'q yoki `0`), keyingi sahifalarda faqat market |
| `catalog` | **Eski holat** (katalog yoqilgan bo'lsa): faqat katalog, javob shakli o'zgarmagan: `{"count", "items"}` (`market` va `next_cursor` yo'q; elementlarda `source` va `address: null` qo'shilgan) |
| `market` | Faqat Market NFT'lari |

**Katalog yashirin bo'lsa** (admin sozlamasi, standart): `/v1/nft` katalog elementlarini umuman qaytarmaydi, ro'yxatda faqat `source: "market"` elementlar bo'ladi. `source=catalog` xatosiz `{"count": 0, "items": []}` qaytaradi. Katalogda ham bor NFT bu holda market nusxasi bilan ko'rinadi. Katalog yoqilgan bo'lsa yuqoridagi jadval avvalgidek ishlaydi.

Har bir elementda `source` (`"catalog"` yoki `"market"`) va `address` bor. Katalog elementida `id` bor, `address` esa `null`: ijaraga `id` bilan `POST /v1/rentals` orqali olinadi. Market elementida `id: null`, `address` — NFT manzili: ijaraga `address` bilan olinadi (narxni so'rang, 4.9, keyin buyurtma bering). Bir NFT ikki marta chiqmaydi: katalogda bor bo'lsa, market nusxasi tashlanadi. **Eski kodingiz `items` ni faqat `id` bilan ishlatsa, `?source=catalog` yuboring.**

**Filtrlar** ikkala manbada bir xil ishlaydi: `kind` (`username`, `number`, `gift`, `other`), `collection`, `backdrop`, `available` (market NFT'lari doim bo'sh, filtr ularni olib tashlamaydi), `q` (nom bo'yicha qidiruv, `@` va harf kattaligi e'tiborsiz, 64 belgigacha). Market uchun qo'shimcha: `category` (`gifts` (standart), `usernames`, `numbers`; `kind` berilsa `kind` ustun), `limit` (1–100, standart 50), `cursor` (javobdagi `next_cursor`, o'zgartirmasdan). Katalog sahifalanmaydi: u hammasi 1-sahifada keladi.

**Qat'iylik.** Noto'g'ri `source` har doim `400`. `source` aniq berilganda (`catalog`, `market`, `all`) noto'g'ri `q`, `limit`, `cursor` yoki `category` ham `400`. `source` berilmasa (standart) ular e'tiborsiz qoldiriladi: masalan `limit=500` 100 ga qisiladi, noto'g'ri `cursor` 1-sahifani beradi.

**`market` obyekti** (`source=catalog` dan boshqa hamma holatda keladi):

| Maydon | Ma'nosi |
|---|---|
| `available` | `true` bo'lsa market ro'yxati qo'shildi. `false` bo'lsa javob **xatosiz** `200`, `items` da faqat katalog (`source=market` da bo'sh) |
| `reason` | Faqat `available: false` da: `disabled` (xizmat o'chiq), `rate_limited` (kalit limiti), `unavailable` (market vaqtincha javob bermayapti yoki kurs noma'lum), `preparing` (ro'yxat tayyorlanmoqda) |
| `category`, `total` | Tanlangan bo'lim (`all` yoki bittasi) va filtrdan keyin market NFT'lari soni |
| `updated_at`, `complete` | Ro'yxat yangilangan vaqt (unix) va to'liqmi. Market mavjud bo'lmasa `null` va `false` |

`price` — mijozga ko'rsatiladigan kunlik narx (so'm), to'lov: `price × days + fee`. Market ro'yxati bir necha daqiqa eskirgan bo'lishi mumkin (`updated_at`): NFT band bo'lib qolsa, narx so'raganda yoki buyurtmada `409 rejected` qaytadi.

**Limit.** Bitta API kalit bo'yicha daqiqasiga 120 so'rov (barcha `GET /v1/market/*` bilan birga). `source=market` yoki `all` aniq berilganda oshsa `429 rate_limited` + `Retry-After`. `source` berilmasa (standart) 429 **qaytmaydi**: javob `200`, katalog bilan, `market.available: false`, `reason: "rate_limited"`. `source=catalog` limitga sanalmaydi. API kalit mijozga bog'lanmagan bo'lsa ham katalog (va market) olinadi.

```json
{
  "count": 2,
  "items": [
    { "id": 1, "address": null, "source": "catalog", "name": "Scared Cat", "kind": "other", "type": "gift",
      "image": "https://nft.fragment.com/gift/scaredcat-14805.webp", "lottie": "https://nft.fragment.com/gift/scaredcat-14805.lottie.json",
      "price": 60000, "fee": 3000, "currency": "UZS", "available": true },
    { "id": null, "address": "EQ...", "source": "market", "name": "@nomoz", "kind": "username", "type": "username",
      "image": "https://nft.fragment.com/username/nomoz.webp", "lottie": null,
      "price": 20000, "fee": 3000, "currency": "UZS", "available": true, "min_days": 1, "max_days": 180,
      "attributes": [], "links": { "telegram": "https://t.me/nomoz", "site": null, "fragment": "https://fragment.com/username/nomoz" }, "gift": null }
  ],
  "next_cursor": "1",
  "market": { "available": true, "category": "usernames", "total": 3625, "updated_at": 1793932877, "complete": true }
}
```

Market mavjud bo'lmaganda (masalan o'chiq) ham javob `200`, faqat katalog:

```json
{
  "count": 1,
  "items": [ { "id": 1, "address": null, "source": "catalog", "name": "Scared Cat" } ],
  "next_cursor": null,
  "market": { "available": false, "reason": "disabled", "category": "all", "total": 0, "updated_at": null, "complete": false }
}
```

```bash
curl https://user299.hostx.uz/v1/nft \
  -H "Authorization: Bearer YOUR_API_KEY"

# faqat katalog (eski holat)
curl "https://user299.hostx.uz/v1/nft?source=catalog" \
  -H "Authorization: Bearer YOUR_API_KEY"

# faqat market, username'lar, 20 tadan
curl "https://user299.hostx.uz/v1/nft?source=market&kind=username&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

`?source=catalog` javobi (`200 OK`): tashqi shakl eski, `{count, items}`. Namuna qisqartirilgan: har elementda yuqoridagi maydonlar ham keladi, jumladan `source: "catalog"` va `address: null`:

```json
{
  "count": 4,
  "items": [
    {
      "id": 1,
      "name": "Scared Cat",
      "image": "https://nft.fragment.com/gift/scaredcat-14805.webp",
      "lottie": "https://nft.fragment.com/gift/scaredcat-14805.lottie.json"
    },
    {
      "id": 2,
      "name": "Vintage Cigar",
      "image": "https://nft.fragment.com/gift/vintagecigar-2681.webp",
      "lottie": "https://nft.fragment.com/gift/vintagecigar-2681.lottie.json"
    },
    {
      "id": 3,
      "name": "Lol Pop",
      "image": "https://nft.fragment.com/gift/lolpop-19592.webp",
      "lottie": "https://nft.fragment.com/gift/lolpop-19592.lottie.json"
    },
    {
      "id": 4,
      "name": "Santa Hat",
      "image": "https://nft.fragment.com/gift/santahat-61419.webp",
      "lottie": "https://nft.fragment.com/gift/santahat-61419.lottie.json"
    }
  ]
}
```

### 4.2 `GET /v1/nft/{id}`: bitta NFT

| Parametr | Tur | Ma'nosi |
|---|---|---|
| `id` | yo'lda | NFT raqami (`1`), @username yoki nom (4.2.1) **yoki NFT manzili**: `EQ...`, `UQ...`, `0:...` (raw) |
| `address` | so'rovda | Manzilni yo'lga yozmasdan: `GET /v1/nft?address=EQ...`. Noto'g'ri manzil: `400` |
| `source` | so'rovda | Ixtiyoriy: `catalog`, `market`, `all` (standart). Manzil bo'yicha qidiruvni cheklaydi. Noto'g'ri qiymat: `400` |

```bash
curl https://user299.hostx.uz/v1/nft/1 \
  -H "Authorization: Bearer YOUR_API_KEY"

# manzil bo'yicha (katalog yoki market)
curl https://user299.hostx.uz/v1/nft/EQ... \
  -H "Authorization: Bearer YOUR_API_KEY"
curl "https://user299.hostx.uz/v1/nft?address=EQ..." \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Javob: `200 OK` (ro'yxatdagi bitta element bilan bir xil shakl)

```json
{
  "id": 1,
  "address": null,
  "source": "catalog",
  "name": "Scared Cat",
  "image": "https://nft.fragment.com/gift/scaredcat-14805.webp",
  "lottie": "https://nft.fragment.com/gift/scaredcat-14805.lottie.json"
}
```

**Manzil bo'yicha qidiruv tartibi:** avval katalog (`source: "catalog"`, EQ/UQ/raw yozilishidan qat'i nazar bir xil NFT topiladi), topilmasa Market (`source: "market"`, `id: null`, `address` bor). `?source=catalog` faqat katalogda, `?source=market` faqat marketda izlaydi. Raqam va @username faqat katalogdan izlanadi.

| Holat | Javob |
|---|---|
| Topilmadi, yoki `source=market` bilan raqam/@nom, yoki market o'chiq | `404 not_found` |
| `?address=` bo'sh yoki manzil noto'g'ri; `source` noto'g'ri | `400 bad_request` |
| NFT katalogda yo'q, Market ro'yxati hozir mavjud emas (breaker, tayyorlanmoqda, kurs yo'q) | `502 upstream_unavailable` |
| Market izlanganda kalit limiti (daqiqasiga 120) oshdi | `429 rate_limited` + `Retry-After` |

Katalogdagi NFT (katalog yoqilgan bo'lsa) har doim market holatiga qaramasdan topiladi. Bunday `id` yo'q bo'lsa, `404` qaytadi. Katalog yashirin bo'lsa raqam va @username bo'yicha so'rov doim `404`, manzil bo'yicha so'rov esa to'g'ridan-to'g'ri Marketdan izlanadi.

### 4.2.1 Turlar va username bo'yicha izlash

Har bir NFT'ning `kind` maydoni bor. U blokcheyndagi kolleksiya manziliga qarab
avtomatik aniqlanadi:

| Qiymat | Ma'nosi |
|---|---|
| `username` | Telegram username, masalan `@nomoz` |
| `number` | Telegram anonim raqami, `+888 ...` |
| `other` | Boshqa NFT: gift, oddiy kolleksiya |

**Faqat username'lar:**

```
GET /v1/nft?kind=username
```

Javob ko'rinishi `GET /v1/nft` bilan bir xil (`count`, `items`; `source` berilmasa market ham, 4.1). `kind` filtri katalogga ham, marketga ham qo'llanadi.
Noma'lum qiymat berilsa `400` qaytadi.

**Username bo'yicha bitta NFT:**

```
GET /v1/nft/@nomoz
GET /v1/nft/nomoz        # @ ixtiyoriy
GET /v1/nft/NOMOZ        # katta-kichik harf farq qilmaydi
GET /v1/nft/%40nomoz     # URL-kodlangan ko'rinishi ham ishlaydi
```

Slug bo'yicha ham topiladi. Topilmasa yoki NFT o'chirilgan bo'lsa `404`.

> Raqam va @username bo'yicha bitta NFT faqat **katalogdan** izlanadi. Fragment'dagi istalgan
> username emas, faqat ijaraga qo'yilganlari. Market NFT'i uchun uning manzilini (`EQ...`) ishlating (4.2).

### 4.3 `GET /v1/balance`: balans

```json
{ "balance": 250000, "currency": "UZS" }
```

Balansni admin to'ldiradi. Ijara narxi shu balansdan yechiladi.

### 4.4 `POST /v1/rentals`: NFT'ni ijaraga olish

So'rov body'si (JSON):

```json
{ "nft_id": 1, "days": 3, "link": "https://app.tonkeeper.com/ton-connect?v=2&id=...", "renter_telegram_id": 123456789 }
```

- `days`: 1 dan 365 gacha.
- `link`: ixtiyoriy, lekin tavsiya etiladi. Pastga qarang.
- `renter_telegram_id`: ixtiyoriy, kim ijaraga olganini eslab qolish uchun.

Balansdan `price × days + fee` yechiladi: kunlik narx, kunlar soni va bitta xizmat haqi (NFT qaysi bo'lishidan qat'i nazar bir xil). Javob `201`: ijara obyekti (5.1-bo'lim). Havola buzuq bo'lsa `400`, boshqa xatolar `409` bilan qaytadi, sababi `error.message` da.

**Kalit brauzerga chiqmasin:** bu so'rovni faqat o'z serveringizdan yuboring.

**`link`** — ijarachining Fragment'dagi TON Connect havolasi (qanday olinadi — 4.5-bo'lim).
Ko'rinishi noto'g'ri havola `400` qaytaradi va **balansdan pul yechilmaydi**.

**Havolasiz so'rov:** ijara to'lanadi, lekin **NFT yuborilmaydi** — joyidan qo'zg'almaydi, gaz sarflanmaydi.
Javobda `"awaiting_link": true`. Havolani keyin `POST /v1/rentals/{id}/connect` bilan yuboring: ulangan zahoti
NFT yo'lga chiqadi, ijara muddati shu paytdan boshlanadi. Belgilangan vaqt (standart 30 daqiqa) ichida ulanilmasa,
ijara bekor bo'ladi va **pul to'liq qaytadi**.

**Ulanish urinishlari:** bitta ijara uchun ko'pi bilan **3 marta** (yaratishdagi havola ham hisoblanadi).
Qolgani `connect_attempts_left` da. Limit tugasa `/connect` `429 connect_limit` qaytaradi.
Ko'rinishi noto'g'ri havola urinish hisoblanmaydi.

**Havola bilan:** Rentora havolani olgan zahoti **darhol Fragment'ga ulanadi** (havola hali yangi, Fragment sahifasi ochiq).
**Pul faqat ulanish muvaffaqiyatli bo'lgandan keyin yechiladi.** Ulanib bo'lmasa, javob `409` bo'ladi,
sababi `error.message` da, balansga tegilmaydi — ijarachiga Fragment'da yangi havola olishni ayting.

- Ulangach javobda `connected_at` to'la bo'ladi. NFT ijara hamyoniga 10–60 soniyada yetib keladi
  (`queued` → `transferring` → `active`). Shundan keyin ijarachi Fragment sahifasini **yangilasa**, NFT ko'rinadi.
- Ijara muddati ulangan paytdan boshlanadi.
- Fragment ulanishni uzsa (ijarachi "Disconnect" bossa), `status_message` da yangi havola so'raladi.
  Yangisini `POST /v1/rentals/{id}/connect` bilan yuboring.


### 4.5 `POST /v1/rentals/{id}/connect`: Fragment'ga ulash

Havola ijarani yaratishdan **oldin** olinadi. Foydalanuvchi quyidagilarni bajaradi:

1. fragment.com'ga o'z Telegram akkaunti bilan kiradi;
2. **Connect TON** tugmasini bosadi;
3. hamyonlar ro'yxatidan **Tonkeeper**ni tanlaydi;
4. QR kod o'rniga havolani nusxalaydi (`https://app.tonkeeper.com/ton-connect?v=2&id=...`);
5. havolani saytingizdagi formaga qo'yadi.

Sayt havolani `POST /v1/rentals` ga `link` sifatida qo'shadi. `/connect` esa havola eskirganda yoki boshqa qurilmadan kirishda **yangi havola** yuborish uchun:

```json
{ "link": "https://app.tonkeeper.com/ton-connect?v=2&id=..." }
```

Shundan keyin Fragment'da NFT foydalanuvchiniki kabi ko'rinadi. **NFT'ni sotish yoki o'tkazish mumkin emas**, chunki hamyon kaliti ijarachida emas. Havola bir necha daqiqagina amal qiladi.

### 4.6 `GET /v1/rentals` va `GET /v1/rentals/{id}`

Shu kalit egasining ijaralari: ro'yxat (`count`, `items`) yoki bitta ijara obyekti.

### 4.7 Webhook: holat o'zgarganini Rentora o'zi xabar qiladi

Har 10 soniyada so'rov yuborib turishni istamasangiz, kabinetda **webhook manzilini** ko'rsating.
Ijara holati o'zgarganda Rentora shu manzilga `POST` qiladi.

- Manzil `https://` bilan boshlanishi shart. `http://` manzilga xabar yuborilmaydi.
- Xabar keladigan holatlar: `rental.ready`, `rental.active`, `rental.completed`, `rental.cancelled`.
  Oraliq bosqichlar (`queued`, `transferring`, `returning`) uchun xabar yuborilmaydi.

So'rov:

```http
POST /rentora HTTP/1.1
Content-Type: application/json
X-Rentora-Event: rental.ready
X-Rentora-Signature: sha256=8f2b...c41d

{
  "event": "rental.ready",
  "sent_at": "2026-09-26T10:14:02+00:00",
  "rental": { "id": 128, "nft_name": "@nomoz", "status": "ready", "can_connect": true }
}
```

`rental` ichidagi maydonlar 5.1-bo'limdagi ijara obyekti bilan bir xil.

#### Imzoni tekshirish

Manzilingizni boshqa odam ham bilib qolishi mumkin, shuning uchun har bir xabar kabinetdagi
**imzo kaliti** bilan imzolanadi. Imzoni tekshirmasangiz, kimdir soxta "ijara tugadi" xabarini
yuborib qo'yishi mumkin.

```php
$secret = 'whs_...';                       // kabinetdan olinadi
$body   = file_get_contents('php://input');
$mine   = 'sha256=' . hash_hmac('sha256', $body, $secret);
$theirs = $_SERVER['HTTP_X_RENTORA_SIGNATURE'] ?? '';

// hash_equals: solishtirish vaqti orqali sirni oshkor qilmaydi
if (!hash_equals($mine, $theirs)) { http_response_code(401); exit; }

$data = json_decode($body, true);
http_response_code(200);                   // 2xx qaytarish shart
```

Node.js'da imzo **xom (raw) body** bo'yicha hisoblanadi, shuning uchun JSON'ni o'zingiz ochasiz:

```js
app.post('/rentora', express.raw({ type: 'application/json' }), (req, res) => {
  const mine = 'sha256=' + crypto.createHmac('sha256', process.env.RENTORA_SECRET)
                                 .update(req.body).digest('hex');
  const a = Buffer.from(mine), b = Buffer.from(req.get('X-Rentora-Signature') || '');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
  const data = JSON.parse(req.body);
  res.sendStatus(200);
});
```

#### Qayta urinish

`2xx` qaytarsangiz, xabar yetib bordi deb hisoblanadi. Aks holda Rentora **5 martagacha**
qayta yuboradi: 1 daqiqa, 5 daqiqa, 30 daqiqa, so'ngra 2 soat oralig'ida. Shundan keyin ham
yetib bormasa, xabar "yetib bormadi" deb belgilanadi va kabinetning Sozlamalar bo'limida ko'rinadi.

Qayta yuborish bo'lishi mumkin, shuning uchun bitta xabarni ikki marta qabul qilishga tayyor
bo'ling: `rental.id` va `event` bo'yicha tekshirib, takroriy xabarni e'tiborsiz qoldiring.

Kabinetdagi **"Sinov xabari yuborish"** tugmasi `"event": "test"` bilan bitta xabar yuboradi —
sozlamani ishga tushirmasdan tekshirib olasiz.

### 4.8 NFT egasi: o'z NFT'ingizni ijaraga berish

Xuddi shu API kalit bilan mijoz ham NFT'ni **ijaraga oladi**, ham o'z NFT'ini **ijaraga beradi**.
Mijozga alohida hamyon ochiladi. NFT o'sha hamyonda turadi, ijaraga berilganda undan chiqadi va
muddat tugagach **o'sha hamyonga qaytadi**. Ijara narxidan platforma ulushi (`commission_pct`, odatda 10%)
ayrilib, qolgani **daromad balansiga** (`earnings`) yoziladi. Xizmat haqi (`fee`) daromadga kirmaydi.
Hammasini brauzerda ham qilish mumkin: `https://user299.hostx.uz/egasi` (NFT egasi paneli).

Tartib:
1. `POST /v1/owner/wallet` — hamyon (24 so'z **faqat shu javobda**).
2. `POST /v1/owner/activate` — faollashtirish: hamyondan **0.10 TON** Rentora'ga yechiladi (ijaraga berish huquqi, qaytarilmaydi; hamyonda kamida 0.12 TON kerak).
3. Hamyonga NFT va **kamida 0.2 TON** yuboriladi.
4. `POST /v1/owner/nfts` — katalogga qo'shish **so'rovi**. Admin ko'rib, nom/rasm/narxni to'ldirib tasdiqlaydi — shundan keyin NFT katalogda chiqadi.
5. Ijara tugagach daromad yoziladi; `POST /v1/owner/withdrawals` bilan yechiladi.

Ikki balans ALOHIDA:

| Balans | Nima uchun | Qayerda |
|---|---|---|
| `balance` | NFT ijaraga **olish** uchun to'lanadi | `GET /v1/balance` |
| `earnings` | NFT'lar ijaradan **ishlab topgan** pul | `GET /v1/owner/earnings` |

**0.2 TON qoidasi.** Har bir ijarada NFT'ni yuborish va qaytarish gazi **egasining hamyonidan** to'lanadi.
Shuning uchun katalogda NFT turishi uchun hamyonda **kamida 0.2 TON** bo'lishi shart. TON kamaysa yoki NFT
hamyondan olib chiqilsa, NFT **avtomatik katalogdan olinadi** va ijaraga berilmaydi. Qayta to'ldirilgach
bir necha daqiqada o'zi qaytadi.

| Metod va yo'l | Body | Nima qiladi |
|---|---|---|
| `GET /v1/owner` | — | Holat: `wallet`, `activation` (`status`: none/pending/active, `fee_ton`, `need_ton`, `to`), `ton_balance`, `min_ton`, `gas_ok`, `can_request`, `earnings`, `pending_withdraw`, `min_withdraw`, `commission_pct`, `listings`, `requests` |
| `POST /v1/owner/wallet` | — | Hamyon yaratadi. Javob `201`: `wallet`, `mnemonic` (24 so'z), `min_ton`. Ikkinchi marta `409` |
| `POST /v1/owner/activate` | — | 0.10 TON yechib faollashtiradi. Javob: `activation` obyekti. TON yetmasa yoki takror bo'lsa `409` |
| `GET /v1/owner/nfts` | — | Hamyondagi NFT'lar (blokcheyndan). Katalogdagilarida `listing`, so'rovdagilarida `request` |
| `POST /v1/owner/nfts` | `{"nft_address": "EQ...", "price": 40000, "note": "..."}` | Katalogga qo'shish so'rovi (`price` — taklif, so'm/kun). `201`, `status: pending` |
| `GET /v1/owner/requests` | — | So'rovlar: `pending`, `approved`, `rejected` (sababi `admin_note`), `cancelled` |
| `POST /v1/owner/requests/{id}/cancel` | — | Kutilayotgan so'rovni bekor qiladi |
| `POST /v1/owner/nfts/{id}` | `{"active": false}` yoki `{"active": true}` | Katalogdan oladi yoki qaytaradi. Narxni admin belgilaydi (`price` yuborilsa `400`) |
| `GET /v1/owner/stats?days=30` | — | Qaysi NFT qancha ishladi: `totals`, `items` (`rentals`, `days_rented`, `earned`, `pending`), `daily`. `days`: 7, 30, 90 yoki 0 |
| `GET /v1/owner/earnings` | — | `earnings`, `pending_withdraw`, `items` (harakatlar tarixi) |
| `GET /v1/owner/withdrawals` | — | Yechish so'rovlari: `pending`, `paid`, `rejected` |
| `POST /v1/owner/withdrawals` | `{"amount": 100000, "method": "card", "details": "8600 1234 5678 9012"}` | Pul yechish so'rovi. `201` |

Listing obyekti: `id`, `name`, `kind`, `nft_address`, `image`, `price` (admin belgilagan), `active`, `listed` (katalogda ko'rinadimi),
`paused` (`low_ton` | `nft_missing` | `owner_blocked` | `null`), `paused_message`, `rented_now`, `rentals_count`, `earned`.

Pul yechish usullari (`method`):
- `card` — `details`: 16 raqamli karta. Admin o'tkazib "to'landi" deb belgilaydi.
- `ton` — `details`: TON manzil. Admin yuborib "to'landi" deb belgilaydi.
- `balance` — `details` kerak emas. Pul **darhol** API balansiga o'tadi.

Summa so'rov paytida daromaddan ushlab turiladi. Rad etilsa qaytib tushadi, sababi `admin_note` da.
Eng kam summa `min_withdraw` da (`balance` usuliga taalluqli emas). Ko'rib chiqilmagan so'rovlar 3 tadan oshmaydi.

**AI uchun:** 24 so'zni hech qachon log'ga, bot xabariga yoki bazaga ochiq yozmang — ular bilan hamyon to'liq boshqariladi.
Mijozga ularni qog'ozga yozib olishni ayting. Faollashtirish to'lovi qaytarilmasligini mijozga oldindan ayting.
Egasi o'z NFT'ini ijaraga ololmaydi (`409`).

### Gift bo'limlari: kolleksiya va fon bo'yicha

Rentora katalogga qo'shilgan har bir gift'ni o'zi tegishli bo'limga joylaydi: kolleksiya (Plush Pepe, Durov's Cap…) NFT havolasidan, fon (backdrop) blokcheyndagi metadata'dan olinadi.

- `GET /v1/gifts` — Telegram'dagi barcha kolleksiyalar va fonlar, har birida katalogda nechta NFT bor (`count`). `count: 0` — bo'limga NFT hali qo'shilmagan ("tez orada"). Ro'yxat avtomatik yangilanadi; hali ro'yxatda yo'q yangi kolleksiya `"soon": true` bilan keladi.
- `GET /v1/nft?collection=plushpepe&backdrop=Sapphire` — bo'lim bo'yicha saralash (`kind` bilan ham birga). Noma'lum fon — `400`.
- Har bir NFT javobida `gift` maydoni (gift bo'lmasa `null`):

```json
"gift": {
  "collection": "plushpepe",
  "collection_name": "Plush Pepes",
  "collection_known": true,
  "number": 12,
  "backdrop": { "name": "Sapphire", "colors": ["#58a3c8", "#5379c2"] },
  "model": "Emerald Pepe",
  "symbol": "Crown"
}
```

`backdrop.colors` — Telegram'dagi radial gradient ranglari (markaz, chet). Saytdagi katalogda havola: `/katalog?collection=plushpepe&backdrop=Sapphire`.

### Ma'lumotnomalar: turlar, ranglar, kolleksiyalar, havolalar

Bot menyusi yoki sayt filtrlari uchun tayyor ro'yxatlar (hammasi `GET`, API kalit bilan):

| Endpoint | Nima qaytaradi |
|---|---|
| `GET /v1/catalog/types` | NFT turlari: `kind`, `label`, `count`, `available`, `filter` (`?kind=gift`) |
| `GET /v1/catalog/colors` | Ranglar (gift fonlari): `name`, `center`, `edge` (hex), `css` gradient, `count`. `?all=1` — barcha 60+ rang |
| `GET /v1/catalog/collections` | Gift kolleksiyalari: `slug`, `name`, `thumb`, `count`, `soon`. `?all=1` — hammasi |
| `GET /v1/catalog/links` | Barcha NFT havolalari: `telegram`, `site`, `fragment`, `image`, `lottie`, `emoji_id`, `color` |

Filtrlar (`/v1/nft` va `/v1/catalog/links`): `?kind=username|number|gift|other`, `?collection=plushpepe`, `?backdrop=Sapphire`, `?available=1` — birga ham ishlaydi. Noma'lum qiymat — `400`.

```json
GET /v1/catalog/links?kind=gift&available=1
{
  "count": 1,
  "items": [{
    "id": 12, "name": "Plush Pepe #77", "type": "gift", "available": true, "price": 15000, "currency": "UZS",
    "telegram": "https://t.me/nft/PlushPepe-77",
    "site": "https://user299.hostx.uz/n/plushpepe-77",
    "fragment": "https://fragment.com/gift/plushpepe-77",
    "image": "https://nft.fragment.com/gift/plushpepe-77.webp",
    "lottie": "https://nft.fragment.com/gift/plushpepe-77.lottie.json",
    "emoji_id": null,
    "color": { "name": "Sapphire", "colors": ["#58a3c8", "#5379c2"] }
  }]
}
```

Har bir NFT javobida ham endi `type` va `links` (`telegram`, `site`, `fragment`) maydonlari bor.

### 4.9 Market: NFT manzili bilan ijaraga olish

Katalogda ko'rinmagan NFT'ni ham `address` (NFT manzili) bilan ijaraga olish mumkin, agar u hozir ijaraga qo'yilgan bo'lsa. Ijaraga qo'yilmagan yoki band NFT uchun `409 rejected` qaytadi (pul yechilmaydi). Tartib: avval narxni so'rang (pul yechilmaydi), keyin shu narx bilan buyurtma bering. Narx so'mda, balansdan yechiladi. Ijaraga qo'yilgan NFT'larni `GET /v1/nft?source=market` (4.1) dan toping, `address` ni shu javobdan oling; bitta NFT: `GET /v1/nft/{address}` (4.2). Bu bo'limdagi so'rovlar bitta API kalit bo'yicha sanaladi: `GET` daqiqada 120, `POST` daqiqada 10 (`GET /v1/nft` ning market qismi ham shu `GET` hisobiga kiradi). Oshsa `429 rate_limited` + `Retry-After`.

**Narxni so'rash:** `GET /v1/market/quote?address=...&days=3` (`days` 1–365; daqiqasiga 40 tagacha)

```json
{
  "address": "EQ...", "name": "@nomoz", "days": 3,
  "per_day_uzs": 20000, "fee_uzs": 3000, "network_uzs": 0,
  "total_uzs": 63000, "balance": 100000, "enough": true, "missing": 0,
  "pay_enabled": true, "currency": "UZS"
}
```

`total_uzs` to'lanadigan summa. `enough` balans yetishini, `missing` yana qancha so'm kerakligini bildiradi. `pay_enabled: false` bo'lsa xizmat vaqtincha o'chiq (buyurtma `409 rejected` bo'ladi).

**Buyurtma berish:** `POST /v1/market/orders`

- Sarlavha `Idempotency-Key` majburiy: 16–64 belgi (harf, raqam, `_`, `-`). Har yangi buyurtmaga yangi tasodifiy kalit.
- Body: `{"address": "EQ...", "days": 3, "shown_total": 63000}`. `shown_total` foydalanuvchiga ko'rsatilgan `total_uzs`.
- Buyurtma urinishi 10 daqiqada 5 tadan oshmasin.

```bash
curl -X POST https://user299.hostx.uz/v1/market/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 3f9c1a52b7d84e60a1c2d3e4f5a6b7c8" \
  -d '{"address": "EQ...", "days": 3, "shown_total": 63000}'
```

`201 Created`:

```json
{
  "state": "processing",
  "order": { "id": 41, "status": "paid", "nft_name": "@nomoz", "address": "EQ...", "days": 3, "price_uzs": 63000, "created_at": "2026-11-05 10:00:00" },
  "balance": 37000
}
```

`state`: `processing` (NFT yo'lda) yoki `bought` (tayyor).

**Idempotency-Key qoidalari**
- Javob kelmasa, xuddi shu kalit va xuddi shu body bilan qayta yuboring: ikkinchi buyurtma yaratilmaydi, avvalgi natija qaytadi.
- Boshqa `address`, `days` yoki `shown_total` bilan bir kalit: `422 idempotency_mismatch`.
- Buyurtma hozir bajarilayotgan bo'lsa: `409 idempotency_in_progress` + `Retry-After`, shuncha soniya kutib xuddi shu so'rovni qayta yuboring.
- `409 price_changed`: narx ko'rsatilganidan oshgan, javobda yangi `price_uzs`. Pul yechilmaydi. Tasdiqlansa `shown_total` ni yangilab, YANGI kalit bilan yuboring.
- `500 order_unknown`: pul yechilgan, natija noma'lum. Shu kalit bilan qayta YUBORMANG, `GET /v1/market/orders` bilan holatni tekshiring.

**Buyurtmalar:** `GET /v1/market/orders?limit=50&before_id=...` (`limit` 1–100, standart 50, eng yangisi birinchi)

```json
{ "count": 1, "items": [ { "id": 41, "status": "bought", "nft_name": "@nomoz", "address": "EQ...", "days": 3, "price_uzs": 63000, "created_at": "2026-11-05 10:00:00" } ], "next_before_id": null }
```

`next_before_id` bo'sh bo'lmasa, uni `before_id` qilib yuborib keyingi sahifani oling. Bitta buyurtma: `GET /v1/market/orders/{id}`. Boshqa mijozniki yoki yo'q `id`: `404 not_found`.

**Mening ijaralarim:** `GET /v1/market/rentals?limit=50&cursor=...` (`limit` 1–100, standart 50). NFT bo'yicha birlashtirilgan; avval faol ijaralar, keyin qolganlari.

```json
{ "count": 1, "items": [ { "name": "Plush Pepe #42", "address": "EQ...", "status": "active", "end_time": 1793917933, "remaining_sec": 432000,
  "days_total": 5, "price_uzs": 222222, "extending": false, "can_extend": true, "last_at": "2026-11-05 10:00:00" } ], "next_cursor": null }
```

`status`: `active`, `processing`, `ended`, `failed`, `refunded`. `next_cursor` bo'sh bo'lmasa, uni o'zgartirmasdan `cursor` qilib yuboring.

**Ijarani uzaytirish** (faqat o'zingizning tugamagan ijarangiz; NFT qaytmaydi, faqat muddat suriladi):

- Narx: `GET /v1/market/rentals/{address}/extend/quote?days=3`. Javob `GET /v1/market/quote` bilan bir xil maydonlarda. Pul yechilmaydi.
- Uzaytirish: `POST /v1/market/rentals/{address}/extend`, sarlavha `Idempotency-Key` (majburiy), body `{"days": 3, "shown_total": 183600}`. 10 daqiqada 5 urinish.

```bash
curl -X POST "https://user299.hostx.uz/v1/market/rentals/EQ.../extend" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 8d1f0c9a3b6e4f72a5c1d0e9b8a7f6e5" \
  -d '{"days": 3, "shown_total": 183600}'
```

`201 Created`: `{"state": "bought", "order": {...}, "balance": 816400}`. `state`: `bought` yoki `processing`. `Idempotency-Key`, `price_changed`, `order_unknown` qoidalari buyurtmadagi bilan bir xil.

- NFT begonaniki, mavjud emas yoki muddati tugagan bo'lsa: `404 not_found`, javob uch holatda ham bir xil. Pul, kalit va urinish sarflanmaydi. Muddati tugagan NFT'ni yangi buyurtma sifatida oling.
- Uzaytirish buyurtmasi `GET /v1/market/orders` da chiqmaydi; yangi muddat `GET /v1/market/rentals` dagi `end_time` da.

**Webhook hodisalari:** `order.bought` (buyurtma bajarildi), `order.extended` (uzaytirish bajarildi) va `order.failed` (bajarilmadi, pul balansga qaytdi). `order.extended` dagi `order.id` uzaytirish buyurtmasiniki: `GET /v1/market/orders/{id}` uni bermaydi (404), muddatni `GET /v1/market/rentals` dan oling. Imzo va qayta urinish 4.7 bo'limidagi bilan bir xil. Noaniq holatlarda xabar yuborilmaydi; bir buyurtma uchun hodisa bir marta yuboriladi.

```json
{ "event": "order.bought", "sent_at": "2026-11-05T10:00:00+00:00",
  "order": { "id": 41, "status": "bought", "nft_name": "@nomoz", "address": "EQ...", "days": 3, "price_uzs": 63000, "created_at": "2026-11-05 10:00:00" } }
```

## 5. NFT obyekti

| Maydon | Tur | Ma'nosi |
|---|---|---|
| `id` | son | NFT raqami |
| `name` | matn | NFT nomi |
| `image` | matn | Rasm (`.webp`) manzili |
| `kind` | matn | NFT turi: `username`, `number` yoki `other` |
| `lottie` | matn | Animatsiya (Lottie JSON) manzili |
| `price` | son | Ijaraning 1 kunlik narxi (so'm) |
| `fee` | son | Har bir ijaraga bir marta qo'shiladigan xizmat haqi (so'm) |
| `currency` | matn | Valyuta, hozircha `UZS` |
| `available` | mantiqiy | `true` bo'lsa NFT hozir bo'sh va ijaraga olish mumkin |

### 5.1 Ijara obyekti

| Maydon | Tur | Ma'nosi |
|---|---|---|
| `id` | son | Ijara raqami |
| `nft_id`, `nft_name` | son, matn | Qaysi NFT |
| `days` | son | Muddat, kun |
| `price_paid` | son | Balansdan yechilgan umumiy summa |
| `rental_price` | son | Shundan ijara narxi |
| `fee` | son | Shundan xizmat haqi |
| `status` | matn | `queued` → `transferring` → `ready` → `active` → `returning` → `completed`, yoki `cancelled` |
| `status_message` | matn | Holatning o'zbekcha izohi, foydalanuvchiga ko'rsatsa bo'ladi |
| `can_connect` | mantiqiy | `true` bo'lsa Fragment havolasini yuborish mumkin |
| `ends_at` | matn | Ijara tugaydigan vaqt |
| `connected_at` | matn | Fragment'ga ulangan vaqt. `null` bo'lsa, hali ulanmagan |
| `refunded` | mantiqiy | `true` bo'lsa, ijara ishlatilmagani uchun pul qaytarilgan |
| `autoconnect` | mantiqiy | `true` bo'lsa, yaratishda berilgan havola bilan o'zi ulanadi |

## 6. Saytda ko'rsatish

Sxema:

```
Brauzer  --GET /api/nft-->  Sizning server  --GET https://user299.hostx.uz/v1/nft + kalit-->  Rentora API
Brauzer  <--JSON----------  Sizning server  <--JSON---------------------  Rentora API
```

### 6.1 Server: `/api/nft`

Server so'rovni kalit bilan Rentora'ga uzatadi va javobni o'zgartirmay qaytaradi. Foydalanuvchi tilini tanlang: PHP, Node.js yoki Python.

**PHP** (`nft.php`):

```php
<?php
// nft.php: brauzer shu faylga murojaat qiladi, kalit faqat shu yerda turadi
$apiKey = "YOUR_API_KEY";

$ch = curl_init("https://user299.hostx.uz/v1/nft");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

header("Content-Type: application/json; charset=utf-8");
if ($body === false) {
    http_response_code(502);
    exit('{"error":{"code":"server_error","message":"Rentora javob bermadi"}}');
}
http_response_code($code);
header("Cache-Control: public, max-age=60");
echo $body;
```

**Node.js** (`server.js`):

```js
// server.js (Node.js 18+, "type": "module", npm i express)
import express from "express";

const app = express();
const API_KEY = "YOUR_API_KEY";

app.get("/api/nft", async (req, res) => {
  try {
    const r = await fetch("https://user299.hostx.uz/v1/nft", {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    res.status(r.status).json(await r.json());
  } catch {
    res.status(502).json({ error: { code: "server_error", message: "Rentora javob bermadi" } });
  }
});

app.use(express.static("public")); // sahifa public/index.html da turadi
app.listen(3000);
```

**Python** (`app.py`):

```python
# app.py (pip install flask requests)
import requests
from flask import Flask, jsonify

app = Flask(__name__, static_folder="public", static_url_path="")
API_KEY = "YOUR_API_KEY"

@app.get("/")
def home():
    return app.send_static_file("index.html")  # public/index.html

@app.get("/api/nft")
def nft():
    try:
        r = requests.get(
            "https://user299.hostx.uz/v1/nft",
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=10,
        )
    except requests.RequestException:
        return jsonify(error={"code": "server_error", "message": "Rentora javob bermadi"}), 502
    return jsonify(r.json()), r.status_code
```

### 6.2 Sahifa: NFT'larni chizish

Sahifa faqat o'zining `/api/nft` manziliga murojaat qiladi. Bu yerda kalit yo'q. Matn `textContent` orqali qo'yiladi (xavfsiz, `innerHTML` ishlatilmaydi).

```html
<div id="nfts"></div>

<script>
async function loadNfts() {
  const box = document.getElementById("nfts");
  const res = await fetch("/api/nft");
  if (!res.ok) {
    box.textContent = "NFT'larni olib bo'lmadi";
    return;
  }

  const { items } = await res.json();
  for (const nft of items) {
    const card = document.createElement("figure");

    const img = document.createElement("img");
    img.src = nft.image;
    img.alt = nft.name;
    img.width = 200;

    const name = document.createElement("figcaption");
    name.textContent = nft.name;

    card.append(img, name);
    box.append(card);
  }
}
loadNfts();
</script>
```

Serverdan kelgan xato brauzerga o'zgarishsiz uzatiladi, shuning uchun sahifada `res.ok` ni tekshiring.

### 6.3 Animatsiya (Lottie)

Rasm uchun `image` manzilini oddiy `<img>` ga qo'ying. Animatsiya Lottie JSON, uni [lottie-web](https://github.com/airbnb/lottie-web) bilan o'ynating:

```js
// sahifada lottie-web yuklangan bo'lsin
lottie.loadAnimation({
  container: document.getElementById("nft"),
  renderer: "svg",
  loop: true,
  autoplay: true,
  path: nft.lottie,
});
```

Brauzer animatsiya faylini to'g'ridan-to'g'ri ola olmasa (CORS xatosi chiqsa), uni o'z serveringiz orqali bering. Shu vaqtgacha `image` rasmini ko'rsatib turing.

Proksi namunasi (`lottie.php`). Brauzer `lottie.php?f=scaredcat-14805` deb so'raydi, server faylni Fragment'dan olib beradi. Nom regex bilan tekshiriladi, shuning uchun bu orqali boshqa saytga so'rov yuborib bo'lmaydi:

```php
<?php
$f = $_GET["f"] ?? "";
if (!preg_match('/^[a-z0-9]+-\d+$/', $f)) {
    http_response_code(400);
    exit("bad request");
}

$ch = curl_init("https://nft.fragment.com/gift/$f.lottie.json");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_TIMEOUT        => 10,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($body === false || $code !== 200) {
    http_response_code(502);
    exit("upstream error");
}

header("Content-Type: application/json; charset=utf-8");
header("Cache-Control: public, max-age=86400");
echo $body;
```

Sahifada `path` o'rniga shu manzil beriladi: `path: "lottie.php?f=" + slug`. Bu yerda `slug` `lottie` manzilidagi nom, masalan `scaredcat-14805`.

## 7. Xatolar

Muvaffaqiyatli so'rov `200 OK` bilan tugaydi. Xatoda status kodi o'zgaradi va javobda sababi yoziladi:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "API kalit noto'g'ri yoki yuborilmagan"
  }
}
```

| Status | Kod | Sababi va nima qilish kerak |
|---|---|---|
| 400 | `bad_request` | So'rov noto'g'ri: `address`, `days`, `shown_total`, `limit` yoki `before_id` yaroqsiz, yoki `POST /v1/rentals` da `link` buzuq. Pul yechilmaydi. `error.message` nimani tuzatishni aytadi. |
| 400 | `idempotency_key_required` | `POST /v1/market/orders` yoki `POST /v1/market/rentals/{address}/extend` da `Idempotency-Key` yo'q. Har buyurtma yoki uzaytirishga yangi tasodifiy kalit yuboring. |
| 400 | `idempotency_key_invalid` | `Idempotency-Key` 16–64 belgidan iborat bo'lishi kerak (harf, raqam, `_`, `-`). |
| 401 | `unauthorized` | Kalit yo'q yoki noto'g'ri. `Authorization` sarlavhasini tekshiring. |
| 402 | `insufficient_balance` | Balans yetarli emas. Javobda `price_uzs`, `balance`, `missing` bor: `missing` yana qancha so'm kerakligini bildiradi. Balansni to'ldirib, qayta yuboring. Pul yechilmaydi. |
| 403 | `forbidden` | Kalit bu IP manzildan ishlatilishi taqiqlangan (kabinetdagi IP ro'yxati). |
| 403 | `no_client` | Kalit kabinetdagi mijozga bog'lanmagan. Kabinetdan olingan kalit kerak. |
| 404 | `not_found` | Bunday `id` li NFT, ijara yoki buyurtma yo'q (boshqa mijozniki ham shu javobni beradi). Uzaytirishda: bu NFT sizda faol ijarada emas (begona, mavjud emas yoki muddati tugagan: javob bir xil). |
| 409 | `rejected` | Shart bajarilmadi: NFT hozir ijaraga berilmaydi yoki band, xizmat vaqtincha o'chiq, balans yetmaydi (`/v1/rentals`) va hokazo. Pul yechilmaydi. Sababi `error.message` da. |
| 409 | `price_changed` | Narx ko'rsatilganidan oshib ketgan. Javobda yangi `price_uzs`, `per_day_uzs`, `days`. Tasdiqlansa, `shown_total` ni o'zgartirib, yangi `Idempotency-Key` bilan yuboring. |
| 409 | `order_failed` | Pul yechilgan, lekin buyurtma bajarilmadi. `refunded: true` bo'lsa pul balansga qaytgan, `false` bo'lsa tez orada qaytadi. Javobda `balance` va `order_id` ham bor. |
| 409 | `idempotency_in_progress` | Shu kalit bilan buyurtma hozir bajarilmoqda. `Retry-After` soniyadan keyin xuddi shu so'rovni qayta yuboring. |
| 422 | `idempotency_mismatch` | Bu `Idempotency-Key` boshqa parametrlar bilan ishlatilgan. Yangi buyurtma uchun yangi kalit oling. |
| 429 | `rate_limited` | So'rovlar juda ko'p. `Retry-After` sarlavhasida ko'rsatilgan soniya kutib, qayta yuboring. Sahifa har ochilganda emas, natijani serveringizda bir necha daqiqa saqlab bering. |
| 429 | `connect_limit` | `POST /v1/rentals/{id}/connect`: bitta ijara uchun ulanish urinishlari tugagan. Qayta urinish foydasiz, holatni `GET /v1/rentals/{id}` bilan tekshiring. |
| 429 | `too_many_attempts` | Noto'g'ri kalit bilan bitta IP'dan juda ko'p urinish. `Retry-After` soniyadan keyin urinib ko'ring. |
| 500 | `order_unknown` | Pul yechilgan, natija hali noma'lum. Bu kalit bilan qayta **yubormang**: buyurtma uchun `GET /v1/market/orders`, uzaytirish uchun `GET /v1/market/rentals` orqali holatni tekshiring. |
| 500 | `server_error` | Xato Rentora tomonida. Birozdan keyin qayta urinib ko'ring. |
| 502 | `upstream_unavailable` | Ijara xizmati hozir javob bermayapti. Pul yechilmadi, birozdan keyin qayta urinib ko'ring. |
| 503 | `unavailable` | Xizmat vaqtincha ishlamayapti (baza ulanmadi). Birozdan keyin qayta urinib ko'ring. |

## 8. Tekshiruv ro'yxati

- [ ] Kalit faqat server kodida, brauzer kodida yo'q.
- [ ] `POST /v1/rentals` ga ijarachining Fragment havolasi (`link`) qo'shilgan.
- [ ] So'rov manzili `https://user299.hostx.uz/v1` bilan boshlanadi.
- [ ] `Authorization: Bearer ...` sarlavhasi yuborilgan.
- [ ] Sahifada `res.ok` tekshirilgan, xato holati ko'rsatiladi.
- [ ] Ro'yxat serverda bir necha daqiqaga keshlangan.
- [ ] Animatsiya yuklanmasa, `image` rasmi ko'rsatib turiladi.
- [ ] `POST /v1/market/orders` da har buyurtmaga yangi `Idempotency-Key`, `shown_total` esa so'nggi `total_uzs` bilan yuborilgan.
- [ ] Webhook ishlatsangiz: manzil `https://`, imzo `hash_equals` bilan tekshiriladi, takroriy xabar e'tiborsiz qoldiriladi.


## Ijarani uzaytirish

`POST /v1/rentals/{id}/extend` — body: `{"days": 7}`. Faqat `active` (Fragment'ga ulangan) ijara, tugashiga 10 daqiqadan ko'p qolgan bo'lsa. NFT qaytmaydi, ulanish uzilmaydi, faqat `ends_at` suriladi. Narx = kunlik narx × kun, xizmat haqisiz, balansdan darhol yechiladi. Umumiy muddat 365 kundan oshmaydi. Xato bo'lsa `409` va `error.message`. Javob — yangilangan ijara obyekti.
