From 8f122d3d87c53986f359c0a4eaac00fd9431b47f Mon Sep 17 00:00:00 2001 From: Romain Orsoni Date: Wed, 22 Apr 2026 06:06:24 +0200 Subject: [PATCH 1/4] feat(phase-6.1): SDK 1.0.0 GA (TypeScript + Python), ready to publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote both SDKs from RC to stable 1.0.0 with minor drift fixes. TypeScript (@satrank/sdk) - Add "consider_alternative" to AdvisoryBlock.recommendation union (matches the four server values) - Remove dead ApiClient.getAgentVerdict() (never wired to the public surface) - Rewrite README for the narrow 1.0 surface (SatRank, fulfill, listCategories, resolveIntent, wallet drivers, parseIntent) — the previous README still documented the deprecated SDK 0.x SatRankClient - Narrative: "AI agents" -> "autonomous agents on Bitcoin Lightning" - Version: 1.0.0-rc.1 -> 1.0.0 Python (satrank) - Add "consider_alternative" to AdvisoryBlock.recommendation Literal - Narrative update in pyproject.toml description - Version: 1.0.0rc1 -> 1.0.0 Validation - 125/125 TS tests pass, tsc build + lint green - 116/116 Python tests pass, mypy --strict + ruff green - Live smoke against https://satrank.dev: /api/health 200 (schema v41, 8186 agents), /api/intent/categories shape OK, invalid category surfaces ValidationSatRankError correctly in both SDKs Phase 12C note - AgentSource/BucketSource enum sunset (PR #14) is transparent: neither SDK references the enums. No code change required here. Docs - docs/phase-6.1/SDK-DRIFT-AUDIT.md (S1 deliverable) - docs/phase-6.1/SDK-INTEGRATION-TEST.md (S4 deliverable) - docs/phase-6.1/RELEASE-NOTES-DRAFT.md (S5 deliverable, for manual publish) - docs/phase-6.1/SDK-UPDATE-REPORT.md (S6 deliverable) - sdk/CHANGELOG.md and python-sdk/CHANGELOG.md (new) PUBLISH GATE remains closed: artifacts built locally only (sdk/satrank-sdk-1.0.0.tgz untracked; python-sdk/dist/ gitignored). No npm publish / twine upload / gh release / git tag has been run. See RELEASE-NOTES-DRAFT.md for the manual publication checklist. --- docs/phase-6.1/RELEASE-NOTES-DRAFT.md | 49 ++ docs/phase-6.1/SDK-DRIFT-AUDIT.md | 218 ++++++++ docs/phase-6.1/SDK-INTEGRATION-TEST.md | 71 +++ docs/phase-6.1/SDK-UPDATE-REPORT.md | 105 ++++ python-sdk/CHANGELOG.md | 18 + python-sdk/pyproject.toml | 4 +- python-sdk/satrank/__init__.py | 2 +- python-sdk/satrank/types.py | 2 +- sdk/CHANGELOG.md | 20 + sdk/README.md | 662 ++++--------------------- sdk/package.json | 4 +- sdk/src/client/apiClient.ts | 6 - sdk/src/types.ts | 2 +- 13 files changed, 584 insertions(+), 579 deletions(-) create mode 100644 docs/phase-6.1/RELEASE-NOTES-DRAFT.md create mode 100644 docs/phase-6.1/SDK-DRIFT-AUDIT.md create mode 100644 docs/phase-6.1/SDK-INTEGRATION-TEST.md create mode 100644 docs/phase-6.1/SDK-UPDATE-REPORT.md create mode 100644 python-sdk/CHANGELOG.md create mode 100644 sdk/CHANGELOG.md diff --git a/docs/phase-6.1/RELEASE-NOTES-DRAFT.md b/docs/phase-6.1/RELEASE-NOTES-DRAFT.md new file mode 100644 index 0000000..0b2bfcf --- /dev/null +++ b/docs/phase-6.1/RELEASE-NOTES-DRAFT.md @@ -0,0 +1,49 @@ +# SatRank SDK 1.0.0 — Release Notes (draft) + +> **Draft for manual publication.** Do NOT publish automatically. + +Two SDKs promoted from `1.0.0-rc.1` / `1.0.0rc1` to stable `1.0.0`: +- `@satrank/sdk` (TypeScript, npm) — `sdk/satrank-sdk-1.0.0.tgz` +- `satrank` (Python, PyPI) — `python-sdk/dist/satrank-1.0.0-py3-none-any.whl` + `satrank-1.0.0.tar.gz` + +## Highlights + +- **One verb, hard budget.** `sr.fulfill({ intent, budget_sats })` / `await sr.fulfill(intent=..., budget_sats=...)` runs the full L402 flow: discover, pay, retry, report. Budget is a hard cap across attempts. +- **Three wallet drivers.** `LndWallet` (REST + macaroon), `NwcWallet` (NIP-47 encrypted over Nostr), `LnurlWallet` (LNbits-style HTTP). The driver contract is a two-method protocol: `payInvoice(bolt11, maxFeeSats)` + `isAvailable()`. +- **Typed error hierarchy.** `SatRankError` subclasses map to HTTP statuses (`ValidationSatRankError`, `PaymentRequiredError`, `BalanceExhaustedError`, `RateLimitedError`, …). `isRetryable()` (TS) tags 429/503/504/network/timeout for agent-side backoff. +- **NLP helper (EN).** `parseIntent('find me a cheap weather API for Paris under 50 sats')` → `{ category, keywords, budget_sats }`. Drop-in for `fulfill()`. + +## Changes from RC + +- Added `"consider_alternative"` to `AdvisoryBlock.recommendation` union (both SDKs). Server has always emitted four values; SDKs had three. This is **additive** and non-breaking for consumers pattern-matching on the existing three. +- Removed internal `ApiClient.getAgentVerdict()` in the TS SDK (never wired to the public surface). +- Narrative: "AI agents" → "autonomous agents on Bitcoin Lightning" in descriptions. +- TS README rewritten for the narrow 1.0 `SatRank` class (prior README still documented the 0.x `SatRankClient`). + +## Phase 12C note + +The `AgentSource 'observer_protocol' → 'attestation'` enum rename and the retirement of `BucketSource 'observer'` (Phase 12C, PR #14, currently unmerged) are **transparent to the SDK**. Neither SDK references these enums in its typed surface, so SDK consumers are unaffected whether Phase 12C ships before or after this SDK 1.0. + +## Known issue (not blocking) + +`error.code` differs between SDKs for known HTTP statuses: +- Python preserves the server's upstream `code` (e.g. `INVALID_CATEGORY`). +- TypeScript substitutes the class default (e.g. `VALIDATION_ERROR`). + +Reconciling this is a breaking change for whichever side we adjust and is **deferred to a post-1.0 follow-up**. Consumers pattern-matching on `instanceof` (the recommended path) are unaffected. + +## Verification + +- 116 Python tests ✅, 125 TS tests ✅ +- TS build ✅, Python mypy strict ✅, ruff ✅ +- Live smoke against https://satrank.dev ✅ (`listCategories`, `resolveIntent` invalid path) — see `docs/phase-6.1/SDK-INTEGRATION-TEST.md` + +## Publication checklist (manual — Romain) + +- [ ] `cd sdk && npm publish` (requires npm login with 2FA) +- [ ] `cd python-sdk && twine upload dist/satrank-1.0.0*` (requires PyPI token) +- [ ] `git tag v1.0.0 && git push origin v1.0.0` +- [ ] `gh release create v1.0.0 --draft --notes-file docs/phase-6.1/RELEASE-NOTES-DRAFT.md` +- [ ] Announce on Nostr (SatRank npub) + +None of the above is run by the automated loop. The GATE is explicit and enforced. diff --git a/docs/phase-6.1/SDK-DRIFT-AUDIT.md b/docs/phase-6.1/SDK-DRIFT-AUDIT.md new file mode 100644 index 0000000..533bd47 --- /dev/null +++ b/docs/phase-6.1/SDK-DRIFT-AUDIT.md @@ -0,0 +1,218 @@ +# Phase 6.1 — SDK drift audit (TypeScript + Python) + +**Branche :** `phase-6.1-sdk` (branché depuis `origin/main`) +**Date :** 2026-04-22 +**Base de comparaison prod :** `src/openapi.ts` + `src/app.ts` (endpoint wiring) à `origin/main`. +**Note Phase 12C :** PR #14 pas encore mergé. Les changements d'énum backend (`observer_protocol` → `attestation`, retrait `observer` de `BucketSource`) ne sont pas encore dans main, mais ils **n'impactent pas le SDK** (voir §4). + +--- + +## 1. Méthodes SDK exposées + +### 1.1 TypeScript — `@satrank/sdk` 1.0.0-rc.1 + +Surface publique (`sdk/src/index.ts`) : + +| Classe / fonction | Signature | Depuis | +|--------------------------------|--------------------------------------------------------|--------| +| `new SatRank(opts)` | `SatRankOptions → SatRank` | rc.1 | +| `sr.fulfill(opts)` | `FulfillOptions → Promise` | rc.1 | +| `sr.listCategories()` | `() → Promise` | rc.1 | +| `sr.resolveIntent(input)` | `{category,keywords?,budget_sats?,...} → Promise` | rc.1 | +| `parseIntent(text, opts?)` | subpath `@satrank/sdk/nlp` | rc.1 | +| `LndWallet(opts)` | subpath `@satrank/sdk/wallet` | rc.1 | +| `NwcWallet(opts)` + `parseNwcUri` | subpath `@satrank/sdk/wallet` | rc.1 | +| `LnurlWallet(opts)` | subpath `@satrank/sdk/wallet` | rc.1 | +| `deriveSharedSecret`, `nip04Encrypt/Decrypt` | subpath `@satrank/sdk/wallet` | rc.1 | +| Hiérarchie `SatRankError` | 12 sous-classes (`Balance...`, `Payment...`, etc.) | rc.1 | + +### 1.2 Python — `satrank` 1.0.0rc1 + +Surface publique (`python-sdk/satrank/__init__.py`) : + +| Symbole | Équivalent TS | +|--------------------------------|--------------------------| +| `SatRank(api_base=..., wallet=..., caller=...)` | `new SatRank()` | +| `sr.fulfill(intent=..., budget_sats=..., ...)` | `sr.fulfill()` | +| `sr.list_categories()` | `sr.listCategories()` | +| `sr.resolve_intent(...)` | `sr.resolveIntent()` | +| `LndWallet` / `NwcWallet` / `LnurlWallet` | `@satrank/sdk/wallet` | +| `parse_intent(...)` (satrank.nlp) | `@satrank/sdk/nlp` | +| 12 erreurs typées | miroir TS | + +**Observation :** la surface Python est strictement le miroir de TS. Aucun décalage de méthode. + +--- + +## 2. Endpoints prod consommés par le SDK + +Le SDK est volontairement **narrow** : il n'appelle que 3 endpoints HTTP. + +| Méthode SDK | Verb + path | Auth | +|-----------------------|--------------------------------|--------------------------------| +| `listCategories()` | `GET /api/intent/categories` | aucune (free discovery) | +| `resolveIntent()` | `POST /api/intent` | aucune (free discovery) | +| `fulfill()` (interne) | `POST /api/intent` puis fetch candidat + `POST /api/report` optionnel | L402 sur `/api/report` via `depositToken` | + +`ApiClient` TS (`sdk/src/client/apiClient.ts`) expose aussi `getAgentVerdict()` **non re-exporté** via `SatRank` — mort-code inerte, pas dans la surface publique. À nettoyer en S2 (pas de drift prod, juste déchet interne). + +--- + +## 3. Inventaire exhaustif des endpoints prod (source = `src/openapi.ts`) + +26 endpoints routés sous `/api/*` : + +**Agents / scoring** (non utilisés par le SDK 1.0 narrow) : +- `GET /agent/{hash}` +- `GET /agent/{hash}/verdict` +- `GET /agent/{hash}/history` +- `GET /agent/{hash}/attestations` +- `POST /verdicts` (batch 100) +- `GET /agents/top` +- `GET /agents/search` +- `GET /agents/movers` +- `GET /profile/{id}` + +**Attestations / reports** : +- `POST /attestations` (free, X-API-Key) +- `POST /report` ✅ *utilisé par `fulfill()` auto-report* + +**Système** : +- `GET /health` +- `GET /stats` +- `GET /stats/reports` +- `GET /version` +- `GET /openapi.json` + +**Discovery / intent** ✅ *cœur du SDK 1.0* : +- `GET /intent/categories` ✅ +- `POST /intent` ✅ +- `GET /services`, `/services/best`, `/services/categories` +- `POST /services/register` +- `GET /endpoint/{url_hash}` + +**Opérateurs (Phase 7)** : +- `POST /operator/register`, `GET /operators`, `GET /operator/{id}` + +**Monétisation / paiement** : +- `POST /deposit` (2-phase invoice) +- `POST /probe` (paid, 5 credits) + +**Monitoring / temps-réel** : +- `GET /watchlist` +- `GET /ping/{pubkey}` + +**Conclusion :** le SDK couvre 3/26 endpoints (les 3 stables pour le flow discover-pay-deliver). Les 23 autres sont hors scope 1.0 et doivent le rester (pas de surface chargée). + +--- + +## 4. Drifts identifiés + +### 4.1 Drift narratif (MINOR — user-visible) + +Brief Phase 6.1 demande : `"AI agents"` → `"autonomous agents on Bitcoin Lightning"`. Matches : + +| Fichier | Ligne | Texte actuel | +|-----------------------------------|-------|--------------------------------------------------------------------| +| `sdk/package.json` | 4 | `"SatRank SDK 1.0 — sr.fulfill() for AI agents on Bitcoin Lightning"` | +| `sdk/README.md` | 3 | `Client SDK for the SatRank API. Trust scores for AI agents on Bitcoin Lightning.` | +| `python-sdk/pyproject.toml` | 8 | `"SatRank SDK for AI agents — discover, score, and pay Lightning-native HTTP services"` | + +Aucun autre match dans `sdk/` et `python-sdk/` en dehors de README et métadonnées. + +**Classification :** MINOR (texte marketing/description, pas d'API change). + +### 4.2 README TypeScript désaligné (BREAKING docs) + +`sdk/README.md` décrit une classe `SatRankClient` avec ~20 méthodes (`getScore`, `getTopAgents`, `decide`, `report`, `transact`, `watchNostr`, `deposit`, …) qui **n'existe plus dans `sdk/src/`**. La classe exportée est `SatRank` avec 3 méthodes (`fulfill`, `listCategories`, `resolveIntent`). Le README date de la surface SDK 0.x ; la réécriture Phase 6 (narrow 1.0) n'a pas touché la doc. + +Impact consommateur : +- `import { SatRankClient } from '@satrank/sdk'` échouerait à la compilation → l'import n'est pas dans `index.ts`. +- Tous les exemples du README sont morts. + +**Classification :** BREAKING au niveau documentation (code déjà aligné). Réécriture complète du README obligatoire en S2. + +### 4.3 Union `recommendation` incomplète (MINOR — type drift additif) + +Serveur (`src/types/index.ts:606` + `src/openapi.ts:558`) : +```ts +export type Recommendation = 'proceed' | 'proceed_with_caution' | 'consider_alternative' | 'avoid'; +``` + +SDK TS (`sdk/src/types.ts:43`) : +```ts +recommendation: 'proceed' | 'proceed_with_caution' | 'avoid'; // 'consider_alternative' manquant +``` + +SDK Python (`python-sdk/satrank/types.py:88`) : mêmes 3 valeurs, `consider_alternative` absent. + +Émis serveur via `src/utils/recommendation.ts:44` quand `advisoryLevel === 'orange'`. Endpoint concerné : `POST /api/intent` (bloc `advisory.recommendation` par candidat). + +Impact : un pattern-matching TS exhaustif sur `candidate.advisory.recommendation` rate `consider_alternative` silencieusement. En Python, pas d'erreur runtime (TypedDict permissif) mais type-check `mypy --strict` passera quand même à cause du `Literal` permissif. + +**Classification :** MINOR (ajout d'une valeur à une union — additive côté wire, mais consomme-breaking sur pattern-matching exhaustif TS). Correction obligatoire en S2/S3 pour refléter le contrat serveur. + +### 4.4 Énums backend (aucun impact SDK) + +Changements Phase 12C planifiés (pas encore sur `main` au moment de cet audit) : +- `AgentSource` : `'observer_protocol'` → `'attestation'` (rename) +- `BucketSource` : retrait de `'observer'` (sunset Observer Protocol) + +**Recherche dans SDKs** : `grep -r "observer_protocol\|observer" sdk/ python-sdk/` → **0 matches**. Ni les types wire ni les docstrings ne référencent ces enums. Le SDK consomme uniquement des champs stables (`endpoint_url`, `bayesian.verdict`, `advisory.recommendation`, …). + +**Classification :** NO-OP côté SDK. À mentionner dans CHANGELOG pour transparence, mais aucune édition de code. + +### 4.5 Code mort interne (PATCH — cleanup) + +`sdk/src/client/apiClient.ts:62` : méthode `getAgentVerdict()` implémentée, jamais appelée par `SatRank`. À supprimer en S2 (aligner le client sur la surface publique narrow). + +**Classification :** PATCH interne, pas de drift fonctionnel. + +--- + +## 5. Classification globale + proposition de version + +| Drift | Sévérité | Action | +|--------------------------------|---------------------|---------------------------------| +| Narratif "AI agents" | MINOR | Rewrite descriptions | +| README TS désaligné | BREAKING (docs) | Réécriture complète README | +| Union `recommendation` | MINOR (type additif)| Ajouter `'consider_alternative'` TS + Python | +| Énums backend (12C) | NO-OP | Mention CHANGELOG | +| `getAgentVerdict` mort | PATCH interne | Supprimer méthode ApiClient | + +### Proposition de bump + +Les deux SDKs sont en **RC (1.0.0-rc.1 / 1.0.0rc1)**. Un RC n'a pas de garantie API — un consommateur qui a épinglé `1.0.0-rc.1` accepte des changements. La Phase 6.1 est le bon moment pour **promouvoir en GA 1.0.0** car : + +1. La surface narrow `fulfill()/listCategories()/resolveIntent()` est stable depuis Phase 6 (merge 90ba9c0, 2026-04-19). +2. Les tests SDK sont verts (`sdk/tests/` + `python-sdk/tests/`). +3. Le seul ajout de contrat wire (`consider_alternative`) est additif — un consommateur rc.1 qui ne le match pas explicitement ne casse pas (la valeur arrive comme string au runtime). +4. Le README est mensonger sur la surface publique actuelle — un GA propre corrige le tort. + +**Cibles :** +- TypeScript : `1.0.0-rc.1` → **`1.0.0`** (GA) +- Python : `1.0.0rc1` → **`1.0.0`** (GA, aligné) + +--- + +## 6. Estimation d'effort S2→S6 + +| Étape | Contenu | Estim. | +|-------|------------------------------------------------------|-----------| +| S2 | TS : union `recommendation`, narrative, README rewrite, cleanup `getAgentVerdict`, bump 1.0.0, `npm run build` + `npm test` | 2h | +| S3 | Python : union `recommendation` (typing.Literal), pyproject narrative, pytest, bump 1.0.0 | 1h | +| S4 | Intégration vs `https://satrank.dev/api/health` + `/api/intent/categories` + `/api/agents/top` (2 SDKs) | 1h | +| S5 | `npm pack` + `python -m build`, CHANGELOGs, RELEASE-NOTES-DRAFT | 1.5h | +| S6 | Report final, commit, push, draft PR #15 avec checklist de publication | 1h | + +**Total estimé :** 6.5h. Dans la borne basse du brief user (7-11h). Aucune étape ne dépasse 90 min isolément → pas de report envisageable. + +--- + +## 7. Règles cardinales rappelées (self-check) + +- ❌ **PUBLISH GATE absolu** : `npm publish`, `twine upload`, `gh release create`, `git tag v*` → interdits. S5 produit les artefacts dans `sdk/` et `python-sdk/dist/` uniquement. +- ❌ **LND non-négociable** : aucun `openchannel`, `closechannel`, ni ops LN. +- ✅ Si `/api/health` renvoie 500 en S4 → stop immédiat, log, pas de tentative fix prod. +- ✅ Sur ambiguïté : préférer supprimer que conserver (code mort → delete). +- ✅ Branche `phase-6.1-sdk` → pousser avec draft PR #15 pour review Romain. diff --git a/docs/phase-6.1/SDK-INTEGRATION-TEST.md b/docs/phase-6.1/SDK-INTEGRATION-TEST.md new file mode 100644 index 0000000..df3e002 --- /dev/null +++ b/docs/phase-6.1/SDK-INTEGRATION-TEST.md @@ -0,0 +1,71 @@ +# Phase 6.1 — SDK Integration Test Report + +**Date:** 2026-04-22 +**Target:** https://satrank.dev (prod) +**SDKs:** `@satrank/sdk@1.0.0` (TS), `satrank@1.0.0` (Python) + +## Endpoint health + +| Endpoint | Status | Notes | +|---|---|---| +| `GET /api/health` | 200 | `schemaVersion=41`, `agentsIndexed=8186`, `dbStatus=ok`, `lndStatus=ok` | +| `GET /api/intent/categories` | 200 | `{ "categories": [] }` — registry currently empty, shape matches SDK contract | +| `POST /api/intent` (unknown category) | 400 | `code=INVALID_CATEGORY` — correctly rejected | +| `GET /api/agents/top` | 200 | Not in SDK surface (removed in Phase 6 narrowing); verified wire shape for later reference | + +Prod is healthy. No STOP condition triggered. + +## TypeScript SDK smoke + +Ran `@satrank/sdk@1.0.0` from freshly-built `dist/` against prod: + +```json +{ + "ts_sdk_version": "1.0.0", + "steps": [ + { "name": "listCategories", "ok": true, "category_count": 0, "shape_ok": true }, + { + "name": "resolveIntent(invalid)", + "ok": true, + "threw": "ValidationSatRankError", + "code": "VALIDATION_ERROR", + "statusCode": 400, + "message": "Unknown category \"does/not/exist\". Call GET /api/intent/categories for the current list." + } + ] +} +``` + +## Python SDK smoke + +Ran `satrank@1.0.0` (installed from `python-sdk/` editable) against prod: + +```json +{ + "py_sdk_version": "1.0.0", + "steps": [ + { "name": "list_categories", "ok": true, "category_count": 0, "shape_ok": true }, + { + "name": "resolve_intent(invalid)", + "ok": true, + "threw": "ValidationSatRankError", + "code": "INVALID_CATEGORY", + "message": "Unknown category \"does/not/exist\". Call GET /api/intent/categories for the current list." + } + ] +} +``` + +## Observations + +1. **Shape parity OK.** Both SDKs deserialize `/api/intent/categories` into the documented shape. Empty list is handled without exceptions. +2. **Error-class parity OK.** Both SDKs surface `ValidationSatRankError` for HTTP 400. +3. **Known pre-existing cross-SDK divergence on `error.code`:** + - TS SDK discards the server's `error.code` for known HTTP statuses and uses the class default (`VALIDATION_ERROR`). + - Python SDK preserves the server's `error.code` verbatim (`INVALID_CATEGORY`). + - This is not new drift from Phase 6.1 — it's how both SDKs shipped in Phase 6. Reconciling would be a BREAKING change in whichever we adjust. Flagged for a post-1.0 follow-up; not blocking for this release. +4. **Fulfill path not exercised.** No wallet configured (intentional — no LN ops without approval). The L402 flow is covered by the 116 Python tests + 125 TS tests, both green locally. + +## Verdict + +Integration smoke **GREEN** for both SDKs against prod. Ready to proceed to S5 (local build artifacts). diff --git a/docs/phase-6.1/SDK-UPDATE-REPORT.md b/docs/phase-6.1/SDK-UPDATE-REPORT.md new file mode 100644 index 0000000..e8f8abb --- /dev/null +++ b/docs/phase-6.1/SDK-UPDATE-REPORT.md @@ -0,0 +1,105 @@ +# Phase 6.1 — SDK Update Report (final) + +**Branche :** `phase-6.1-sdk` +**Date :** 2026-04-22 +**Durée réelle :** ~2h (vs. 6.5h estimé en S1) +**Statut :** prêt à publier — **manual GATE non franchie** + +--- + +## Livraisons + +| SDK | Version RC | Version GA | Artefact local | +|--------------------|-------------|------------|------------------------------------------------------| +| `@satrank/sdk` | `1.0.0-rc.1`| `1.0.0` | `sdk/satrank-sdk-1.0.0.tgz` (40.3 kB, 58 files) | +| `satrank` (Python) | `1.0.0rc1` | `1.0.0` | `python-sdk/dist/satrank-1.0.0-py3-none-any.whl` + `satrank-1.0.0.tar.gz` | + +--- + +## Étapes exécutées + +### S1 — Audit drift (complet) + +Document : `docs/phase-6.1/SDK-DRIFT-AUDIT.md` + +5 drifts identifiés, classifiés : +1. **MINOR** — narratif "AI agents" → "autonomous agents on Bitcoin Lightning" (3 fichiers de description) +2. **BREAKING-docs** — README TS désaligné (documentait la classe `SatRankClient` 0.x inexistante) +3. **MINOR** — union `AdvisoryBlock.recommendation` incomplète (3 valeurs au lieu de 4) +4. **NO-OP** — Phase 12C enum sunset transparent au SDK (0 références) +5. **PATCH** — `ApiClient.getAgentVerdict()` mort-code + +### S2 — Port TypeScript (complet) + +Fichiers modifiés dans `sdk/` : +- `src/types.ts` → ajout `"consider_alternative"` à `AdvisoryBlock.recommendation` +- `src/client/apiClient.ts` → suppression `getAgentVerdict()` +- `package.json` → `1.0.0-rc.1` → `1.0.0`, description updated +- `README.md` → réécriture complète (180 lignes) pour la surface `SatRank` 1.0 + +Validation : `npm run build` ✅, `npm test` → 125/125 ✅, `npm run lint` ✅. + +### S3 — Port Python (complet) + +Fichiers modifiés dans `python-sdk/` : +- `satrank/types.py` → ajout `"consider_alternative"` au Literal +- `pyproject.toml` → version `1.0.0rc1` → `1.0.0`, description updated +- `satrank/__init__.py` → `__version__ = "1.0.0"` + +Validation : `pytest` → 116/116 ✅, `mypy --strict` ✅, `ruff check` ✅. + +Note : `python-sdk/README.md` était déjà aligné (1.0 narrow), aucune réécriture nécessaire. + +### S4 — Intégration prod (complet) + +Document : `docs/phase-6.1/SDK-INTEGRATION-TEST.md` + +- `/api/health` → 200 (schema v41, 8186 agents, dbStatus=ok, lndStatus=ok) +- `/api/intent/categories` → 200 `{ categories: [] }` (registry vide mais shape OK) +- `POST /api/intent` sur catégorie inconnue → 400 `INVALID_CATEGORY` → `ValidationSatRankError` dans les 2 SDKs +- `/api/agents/top` → 200 (hors surface SDK, vérifié pour référence) + +Pas de STOP condition. Flow fulfill() non exercé (pas de wallet, pas de LN op). + +### S5 — Artefacts locaux (complet) + +Commandes exécutées : +- `cd sdk && npm pack` → `satrank-sdk-1.0.0.tgz` +- `cd python-sdk && python -m build` → `.whl` + `.tar.gz` + +Livrables additionnels : +- `sdk/CHANGELOG.md` (nouveau) +- `python-sdk/CHANGELOG.md` (nouveau) +- `docs/phase-6.1/RELEASE-NOTES-DRAFT.md` (nouveau) + +### S6 — Report + PR #15 (en cours) + +Ce document + commit sur `phase-6.1-sdk` + push + draft PR. + +--- + +## Anomalie notée (non-blocking) + +Divergence cross-SDK préexistante sur `error.code` pour les statuts HTTP connus : +- **Python** préserve le `code` serveur verbatim (ex. `INVALID_CATEGORY`) +- **TypeScript** substitue le `code` par défaut de classe (ex. `VALIDATION_ERROR`) + +Les consommateurs qui utilisent `instanceof` (voie recommandée dans les deux READMEs) ne sont pas affectés. Reconcilier casse l'un ou l'autre → reporté post-1.0. + +--- + +## PUBLISH GATE — statut + +**FERMÉE.** Aucune des 4 commandes interdites n'a été exécutée : +- ❌ `npm publish` +- ❌ `twine upload` +- ❌ `gh release create` +- ❌ `git tag v*` + `git push origin v*` + +Checklist de publication manuelle disponible dans `docs/phase-6.1/RELEASE-NOTES-DRAFT.md`. Romain ouvre la GATE. + +--- + +## Règle LND + +Aucune opération LN effectuée. `LnurlWallet` / `NwcWallet` / `LndWallet` : tests unitaires avec mocks uniquement. diff --git a/python-sdk/CHANGELOG.md b/python-sdk/CHANGELOG.md new file mode 100644 index 0000000..79a7495 --- /dev/null +++ b/python-sdk/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog — satrank (Python) + +## 1.0.0 — 2026-04-22 + +First stable release, promoted from `1.0.0rc1`. + +### Added +- `AdvisoryBlock.recommendation` Literal now includes `"consider_alternative"` to match the four values emitted by the server (previously three). + +### Changed +- Description updated from "for AI agents" to "for autonomous agents on Bitcoin Lightning" in `pyproject.toml`. +- `__version__` bumped from `"1.0.0rc1"` to `"1.0.0"`. + +### Notes +- Phase 12C enum sunset (`AgentSource 'observer_protocol' → 'attestation'`, `BucketSource` without `'observer'`) is transparent to the SDK: neither enum was referenced in SDK types. +- Public surface (`SatRank`, `fulfill`, `list_categories`, `resolve_intent`, wallet drivers, `parse_intent`) and error hierarchy unchanged from `1.0.0rc1`. +- 116 unit tests green. Live smoke against https://satrank.dev passes (see `docs/phase-6.1/SDK-INTEGRATION-TEST.md`). +- Known pre-existing cross-SDK divergence on `error.code`: Python preserves the server's `error.code` verbatim; TypeScript overrides it with a class default for known HTTP statuses. Flagged for a post-1.0 follow-up; not blocking. diff --git a/python-sdk/pyproject.toml b/python-sdk/pyproject.toml index 90cf4aa..6f2e71d 100644 --- a/python-sdk/pyproject.toml +++ b/python-sdk/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "satrank" -version = "1.0.0rc1" -description = "SatRank SDK for AI agents — discover, score, and pay Lightning-native HTTP services" +version = "1.0.0" +description = "SatRank SDK for autonomous agents on Bitcoin Lightning — discover, score, and pay Lightning-native HTTP services" readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } diff --git a/python-sdk/satrank/__init__.py b/python-sdk/satrank/__init__.py index 5a5c07d..0143d6a 100644 --- a/python-sdk/satrank/__init__.py +++ b/python-sdk/satrank/__init__.py @@ -43,7 +43,7 @@ Wallet, ) -__version__ = "1.0.0rc1" +__version__ = "1.0.0" __all__ = [ "SatRank", diff --git a/python-sdk/satrank/types.py b/python-sdk/satrank/types.py index 5d4ae24..ccb4862 100644 --- a/python-sdk/satrank/types.py +++ b/python-sdk/satrank/types.py @@ -85,7 +85,7 @@ class AdvisoryEntry(TypedDict, total=False): class AdvisoryBlock(TypedDict, total=False): advisory_level: Literal["green", "yellow", "orange", "red"] risk_score: float - recommendation: Literal["proceed", "proceed_with_caution", "avoid"] + recommendation: Literal["proceed", "proceed_with_caution", "consider_alternative", "avoid"] advisories: list[AdvisoryEntry] diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md new file mode 100644 index 0000000..f69f963 --- /dev/null +++ b/sdk/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog — @satrank/sdk + +## 1.0.0 — 2026-04-22 + +First stable release, promoted from `1.0.0-rc.1`. + +### Added +- `AdvisoryBlock.recommendation` now includes `"consider_alternative"` to match the four values emitted by the server (previously three). + +### Changed +- Description updated from "for AI agents on Bitcoin Lightning" to "for autonomous agents on Bitcoin Lightning". +- README rewritten for the narrow 1.0 surface (`SatRank` class, `fulfill()`, `listCategories()`, `resolveIntent()`, wallet drivers, `parseIntent()`). The prior README still documented the deprecated SDK 0.x `SatRankClient` surface. + +### Removed +- Internal `ApiClient.getAgentVerdict()` (dead code — never wired to the public `SatRank` class; corresponding server route is not part of the narrow 1.0 surface). + +### Notes +- Phase 12C enum sunset (`AgentSource 'observer_protocol' → 'attestation'`, `BucketSource` without `'observer'`) is transparent to the SDK: neither enum was referenced in SDK types, so no code changes are required here. +- Public surface, wallet driver contract, and error hierarchy unchanged from `1.0.0-rc.1`. +- 125 unit/integration tests green. Live smoke against https://satrank.dev passes (see `docs/phase-6.1/SDK-INTEGRATION-TEST.md`). diff --git a/sdk/README.md b/sdk/README.md index e68b61e..ad7f434 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -1,624 +1,154 @@ # @satrank/sdk -Client SDK for the SatRank API. Trust scores for AI agents on Bitcoin Lightning. +TypeScript client for [SatRank](https://satrank.dev). One verb — `sr.fulfill()` — that discovers, pays, and reports a Lightning-native HTTP service in a single call with a hard budget guarantee. -Zero dependencies. Uses native `fetch()` (Node.js 18+). +Built for autonomous agents on Bitcoin Lightning. Zero runtime dependencies (uses native `fetch()` on Node 18+). -## 1.0.0-rc.1 — Bayesian migration (breaking) - -Phase 3 replaces the composite 0-100 `score` + five-axis `components` shape -with a Bayesian Beta-Binomial posterior. **There is no cohabitation period.** -Every public endpoint now returns a `bayesian` block alongside its operational -fields; the old `score.total` / `score.components` / `score.confidence` have -been removed entirely from the server, the OpenAPI spec, the NIP-85 event tags -and this SDK. - -### Field map - -| Before (0.x) | After (1.0.0-rc.1) | -|-------------------------------------------|---------------------------------------------------| -| `response.score.total` (0-100) | `response.bayesian.p_success` (0-1) | -| `response.score.components.*` | gone — use `response.bayesian.sources` breakdown | -| `response.score.confidence` (0.1-0.9) | `response.bayesian.ci95_low` / `ci95_high` | -| `verdict: 'SAFE' | 'RISKY' | 'UNKNOWN'` | adds `'INSUFFICIENT'` for cold-start / n_obs < 5 | -| `watchlist.change.score` / `.components` | `watchlist.change.bayesian` (full block) | -| NIP-85 tag `rank` | tag `p_success` (+ `ci95_low`, `ci95_high`, `n_obs`) | - -### Verdict semantics - -`verdict` now comes from the posterior directly: - -- `SAFE` — `p_success ≥ 0.80` with converged posterior (n_obs ≥ 5 across - probes, reports, or paid observations). -- `RISKY` — `p_success < 0.50` with converged posterior. -- `INSUFFICIENT` — new in 1.0.0-rc.1. Emitted when `n_obs < 5`; callers should - treat it like `UNKNOWN` but can surface a different UX (cold-start vs. - "we've never seen this node"). -- `UNKNOWN` — agent not indexed at all. - -`SAFE` is the only value that makes `decide.go === true`. - -### Migration in ~5 min - -```diff -- if (verdict.verdict === 'SAFE' && profile.score.total >= 70) { go(); } -+ if (verdict.verdict === 'SAFE' && verdict.p_success >= 0.80) { go(); } - -- const [up, down] = [top.agents.sort((a,b) => b.score - a.score), ...]; -+ const [up, down] = [top.agents.sort((a,b) => b.bayesian.p_success - a.bayesian.p_success), ...]; -``` - -`RiskProfile.name` values (`established_hub`, `declining_node`, …) and the -operational fields on `/decide` (`pathfinding`, `survival`, `serviceHealth`, -`riskProfile`, `flags`, `reason`) are unchanged — the migration is a score -replacement, not a taxonomy rewrite. - - -## Installation +## Install ```bash npm install @satrank/sdk ``` -## Agent identity: pubkey, hash, normalization +Node 18+ required (native `fetch`). In older runtimes, polyfill `globalThis.fetch` or pass a `fetch` implementation in the constructor. -SatRank identifies agents by `public_key_hash`: a 64-char lowercase hex SHA-256 of the Lightning pubkey **treated as an ASCII string** (not its raw bytes). +## Quickstart ```typescript -import { createHash } from 'node:crypto'; - -// 66-char hex LN pubkey -const pubkey = '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f'; - -// Hash — hash the hex string itself, NOT Buffer.from(pubkey, 'hex') -const hash = createHash('sha256').update(pubkey).digest('hex'); -// => 64-char SHA-256 hex, e.g. '2fc0...' -``` - -Endpoints that accept an agent identifier (`/api/score/:hash`, `/api/profile/:id`, `/api/report`, `/api/decide`'s `target`/`caller`) expect the hash form. The SDK's `transact()`, `decide()`, and `report()` helpers accept either the hex pubkey or the hash — they hash the pubkey client-side using the rule above. +import { SatRank } from '@satrank/sdk'; +import { LndWallet } from '@satrank/sdk/wallet'; -If you compute hashes yourself and see `NOT_FOUND { details: { resource: 'Agent (...)' } }` despite the node being indexed, double-check you hashed the pubkey-as-string and not its raw bytes. - -### Response shapes — `publicKeyHash` vs `hash` vs `pubkey` - -Some endpoints and their responses use different names for the same concept. This is not the SDK being sloppy — it matches the server API surface: - -| Endpoint | Input parameter accepts | Response field | -|---|---|---| -| `/api/agents/top` | — | `publicKeyHash` (64-hex SHA-256) | -| `/api/agents/search` | `q` (alias / partial hash / pubkey) | `publicKeyHash` | -| `/api/agent/:publicKeyHash/verdict` | `:publicKeyHash` accepts **hash OR 66-hex LN pubkey** | verdict payload, no identifier echo | -| `/api/profile/:id` | `:id` accepts **hash OR 66-hex LN pubkey** | `agent.publicKeyHash` and `agent.publicKey` (LN pubkey) side by side | -| `/api/score/:hash` | `:hash` (64-hex only) | `publicKeyHash` | -| `/api/report` | `target`, `reporter` both accept hash OR pubkey | no identifier echo | -| `/api/decide` | `target`, `caller` both accept hash OR pubkey | `publicKeyHash` in score, plus `agent.publicKey` | - -**Rule of thumb**: when you read from SatRank, it always gives you `publicKeyHash` (64-hex SHA-256). When you write to SatRank, anywhere that takes an identifier in the path or body will accept **either** the hash **or** the 66-char LN pubkey — the server hashes the pubkey client-side the same way the SDK does. See `normalizeIdentifier()` in `sdk/src/client.ts` for the exact rule. - -## Quick Start - -```typescript -import { SatRankClient } from '@satrank/sdk'; - -const client = new SatRankClient('https://satrank.dev'); - -// Get an agent's Bayesian trust posterior with full evidence -const score = await client.getScore('a1b2c3...64-char-sha256-hash'); -console.log(score.bayesian.p_success); // 0-1 posterior mean -console.log(score.bayesian.ci95_low, score.bayesian.ci95_high); // 95% credible interval -console.log(score.bayesian.verdict); // 'SAFE' | 'RISKY' | 'UNKNOWN' | 'INSUFFICIENT' -console.log(score.evidence.reputation); // LN+ ratings, centrality ranks - -// Leaderboard — sort by the posterior mean -const top = await client.getTopAgents(10); -for (const agent of top.agents) { - console.log(`${agent.alias}: ${agent.bayesian.p_success.toFixed(3)}`); -} - -// Search -const results = await client.searchAgents('ACINQ'); -``` - -## API Reference - -### `new SatRankClient(baseUrl, options?)` - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `timeout` | `number` | `30000` | Request timeout in ms (covers `/api/decide` worst case with on-demand re-probe) | -| `headers` | `Record` | `{}` | Custom headers (e.g. L402 token) | - -### Methods - -| Method | Returns | Description | -|--------|---------|-------------| -| `getScore(hash)` | `AgentScoreResponse` | Detailed score with evidence | -| `getTopAgents(limit?, offset?)` | `TopAgentsResponse` | Leaderboard | -| `searchAgents(alias, limit?, offset?)` | `SearchAgentsResponse` | Search by alias | -| `getHistory(hash, limit?, offset?)` | `HistoryResponse` | Score history | -| `getAttestations(hash, limit?, offset?)` | `AttestationsResponse` | Received attestations | -| `getStats()` | `NetworkStats` | Global network statistics | -| `getHealth()` | `HealthResponse` | Service health | -| `getVersion()` | `VersionResponse` | Build info | -| `decide(input)` | `DecideResponse` | GO/NO-GO with probabilities + positional pathfinding + targetFeeStability + maxRoutableAmount | -| `report(input)` | `ReportResponse` | Submit payment outcome (success/failure/timeout) | -| `getBatchVerdicts(hashes)` | `BatchVerdictItem[]` | Screen up to 100 targets in one call | -| `getProfile(id)` | `ProfileResponse` | Full agent profile with evidence | -| `getMovers()` | `MoversResponse` | Top score movers (7-day delta) | -| `bestRoute(input)` | `BestRouteResponse` | Batch pathfinding for up to 50 targets, top 3 by composite rank | -| `getVerdict(hash, callerPubkey?)` | `VerdictResponse` | SAFE/RISKY/UNKNOWN verdict with flags and risk profile | -| `submitAttestation(input)` | `CreateAttestationResponse` | Submit a trust attestation (free, requires API key) | -| `transact(target, caller, payFn, options?)` | `TransactResult` | Decide, pay, report in one call (options: walletProvider, amountSats, serviceUrl) | -| `searchServices(params?)` | `{ data, meta }` | Browse L402 services by keyword, category, score, uptime (free) | -| `getCategories()` | `ServiceCategory[]` | List available service categories (free) | -| `deposit(amount)` | `DepositInvoiceResponse` | Request a deposit invoice (21–10,000 sats, free endpoint) | -| `verifyDeposit(paymentHash, preimage)` | `DepositVerifyResponse` | Activate deposit balance after payment | -| `getBalance()` | `number \| null` | Remaining L402 requests from the last response's `X-SatRank-Balance` header | -| `getWatchlist(targets, since?)` | `WatchlistResponse` | One-shot verdict-change poll for up to 50 targets | -| `watchPoll(targets, opts, cb)` | `() => void` | HTTP long-poll wrapper around `getWatchlist` | -| `watchNostr(targets, cb, opts?)` | `() => void` | Subscribe to NIP-85 kind 30382 score updates via 3 relays | - -### L402 Authentication - -Scored endpoints require L402 payment (1 sat = 1 request). Two options: standard L402 (21 sats = 21 requests, auto-invoice) or `POST /api/deposit` (21–10,000 requests in one invoice). Pass the token in headers: - -```typescript -const client = new SatRankClient('https://satrank.dev', { - headers: { - 'Authorization': 'L402 :', - }, +const sr = new SatRank({ + apiBase: 'https://satrank.dev', + wallet: new LndWallet({ + restUrl: 'https://127.0.0.1:8080', + macaroonHex: '...', // admin macaroon (hex) + }), + caller: 'my-agent', }); -// Track remaining balance via X-SatRank-Balance header -console.log(client.getBalance()); // 20, 19, 18... 0 -// At 0, the next call throws SatRankError with code BALANCE_EXHAUSTED -// Drop the Authorization header and retry to get a new invoice -``` - -### Evidence - -The `getScore` response includes verifiable evidence: - -```typescript -const { evidence } = await client.getScore(hash); - -// Transaction sample (5 most recent) -evidence.transactions.sample.forEach(tx => { - console.log(tx.txId, tx.protocol, tx.verified); +const result = await sr.fulfill({ + intent: { category: 'data/weather', keywords: ['paris'] }, + budget_sats: 50, }); -// Lightning Network graph data (null for non-LN agents) -if (evidence.lightningGraph) { - console.log(evidence.lightningGraph.sourceUrl); // mempool.space link -} - -// LN+ reputation (null if no ratings) -if (evidence.reputation) { - console.log(evidence.reputation.positiveRatings); - console.log(evidence.reputation.hubnessRank); - console.log(evidence.reputation.sourceUrl); // lightningnetwork.plus link -} - -``` - -### Decision Breakdown: probability components - -`decide()` returns a **GO/NO-GO** backed by a `successRate` (0–1) and five probability components that each explain a distinct failure mode. `components` are not the 5 composite score factors (volume/reputation/seniority/regularity/diversity) — those live on `getProfile(id).score.components`. Decide components are the five independent sub-probabilities that multiply into `successRate`: - -```typescript -const d = await client.decide({ target, caller, walletProvider: 'phoenix' }); - -// Decision -d.go // true | false -d.successRate // 0–1, ≥ 0.85 → go=true -d.basis // 'empirical' when reports are dense enough, else 'proxy' -d.confidence // 'very_low' | 'low' | 'medium' | 'high' | 'very_high' -d.verdict // 'SAFE' | 'RISKY' | 'UNKNOWN' -d.flags // VerdictFlag[] — human-readable drivers -d.reason // short string, why go / why no-go - -// Probability components — each 0–1, combined into successRate -const c = d.components; -c.trustScore // 0–1, normalized composite agent score -c.routable // 0–1, probability a route exists from the caller to the target -c.available // 0–1, recent HTLC acceptance rate from probes -c.empirical // 0–1, reporter-weighted historical success rate (null-like when sparse) -c.pathQuality // 0–1, hop/latency/fee penalty on the live route - -// Ancillary signals -d.targetFeeStability // 0–1 on the target's own fee snapshots, null when no fee data -d.maxRoutableAmount // highest sats with a known route, null when unknown -d.reportedSuccessRate // raw empirical rate 0–1, null when sparse -d.lastProbeAgeMs // freshness of the underlying probe -d.serviceHealth // { status, httpCode, latencyMs, uptimeRatio, servicePriceSats, ... } | null - -// Risk + survival -d.riskProfile // { name: 'low' | 'medium' | 'high', ... } -d.survival // { verdict: 'stable' | 'at_risk' | 'likely_dead', ... } - -// Positional pathfinding (walletProvider → hub node) -d.pathfinding?.sourceProvider // "phoenix" -d.pathfinding?.sourceNode // "03864ef025fde8fb..." (ACINQ pubkey) -d.pathfinding?.hops // 2 (from phoenix's hub, not from SatRank) -``` - -Need the 5-factor breakdown (volume / reputation / seniority / regularity / diversity)? Call `getProfile(id)` or `getScore(hash)`: - -```typescript -const p = await client.getProfile(target); -p.score.total // 0–100 composite -p.score.components.volume // 0–100, weight 25 % -p.score.components.reputation // 0–100, weight 30 % (5 sub-signals inside: centrality 20, peerTrust 30, routingQuality 20, capacityTrend 15, feeStability 15) -p.score.components.seniority // 0–100, weight 15 % -p.score.components.regularity // 0–100, weight 15 % -p.score.components.diversity // 0–100, weight 15 % -p.survival // same shape as on decide -p.riskProfile // same shape as on decide -p.reports // { total, successes, failures, timeouts, successRate } -p.flags // driver flags -``` - -### Error Handling - -The SDK throws typed subclasses of `SatRankError`. Agents can dispatch on error type instead of inspecting `code` or `message` strings. - -```typescript -import { - SatRankClient, - SatRankError, // base class — catches everything - BalanceExhaustedError, // 402 — token used up, remove Authorization and retry for a new invoice - PaymentPendingError, // 402 — deposit invoice not yet settled, retry after paying - DuplicateReportError, // 409 — report/attestation already submitted within dedup window (1h) - RateLimitedError, // 429 — too many requests from this IP - TimeoutError, // 504 / local abort — request exceeded the client timeout - NetworkError, // no HTTP response (DNS, connection refused, etc.) - ServiceUnavailableError, // 503 — feature disabled (e.g. deposit macaroon missing) -} from '@satrank/sdk'; - -try { - const result = await client.transact(target, caller, payFn, { walletProvider: 'phoenix' }); -} catch (err) { - if (err instanceof BalanceExhaustedError) { - // Buy more requests via /api/deposit or remove the Authorization header for a new 21-sat invoice - } else if (err instanceof DuplicateReportError) { - // Already reported this target within the last hour — treat as success - } else if (err instanceof RateLimitedError || err instanceof TimeoutError) { - // Retryable — back off and retry - if (err.isRetryable()) await backoff(); - } else if (err instanceof SatRankError) { - console.log(err.statusCode, err.code, err.message); - } +if (result.success) { + console.log(result.response_body); + console.log(`Paid ${result.cost_sats} sats to ${result.endpoint_used?.url}`); +} else { + console.log('Failed:', result.error?.code, result.error?.message); } ``` -All SatRankError instances have `.isRetryable()` (true for 429/503/504/network/timeout) and `.isClientError()` (true for 4xx input issues). - -Default timeout is 30 seconds — enough to cover `/api/decide` worst case with on-demand re-probe across all probe tiers. Override via `new SatRankClient(url, { timeout: 60_000 })`. +`fulfill()` handles the full L402 flow: calls `/api/intent` to rank candidates, attempts each in rank order, pays BOLT11 invoices via your wallet driver, retries the request with the L402 token, and optionally reports the outcome to `/api/report`. -## transact(): Decide, Pay, Report in One Call - -The full feedback loop automated. The agent calls `transact()`, the SDK handles decide (pre-flight check), executes your payment callback only if GO, then reports the outcome automatically. Verified reports (with preimage + paymentHash) get 2x weight in future scoring. +## Discovery only (no wallet) ```typescript -import { SatRankClient } from '@satrank/sdk'; +const sr = new SatRank({ apiBase: 'https://satrank.dev', caller: 'explorer' }); -const client = new SatRankClient('https://satrank.dev', { - headers: { 'Authorization': 'L402 :' }, -}); +const cats = await sr.listCategories(); +// { categories: [{ name: 'data/weather', endpoint_count: 12, active_count: 8 }, ...] } -const result = await client.transact( - '03864ef025fd...', // target LN pubkey or SHA-256 hash - '024b550337d6...', // your LN pubkey or hash (the caller) - async () => { - // Your payment function. Only called if SatRank says GO. - // Return { success, preimage?, paymentHash? } for verified reporting. - const payment = await myLnd.sendPayment(targetInvoice); - return { - success: payment.status === 'SUCCEEDED', - preimage: payment.preimage, // enables 2x report weight - paymentHash: payment.paymentHash, // enables verification - }; - }, - { walletProvider: 'phoenix', amountSats: 50000 }, // optional: positional pathfinding -); - -if (result.paid) { - console.log(`Paid successfully, reported to SatRank`); - console.log(`Report weight: ${result.report?.weight}`); -} else { - console.log(`SatRank said NO-GO: ${result.decision.reason}`); - // No payment was attempted, no report was submitted +const res = await sr.resolveIntent({ category: 'data/weather', limit: 10 }); +for (const c of res.candidates) { + console.log(c.rank, c.price_sats, c.service_name, c.bayesian.verdict); } ``` -The `transact()` response includes everything: -- `result.paid`: whether the payment went through -- `result.decision`: the full DecideResponse (successRate, verdict, pathfinding, risk profile) -- `result.report`: the ReportResponse when the report submission succeeded, `null` when the report failed (auth / rate limit / duplicate), `undefined` when NO-GO so no payment was attempted. - -Cost: 1 request from your L402 balance for decide (report is free). Latency: ~500ms + your payment time. - -## Reporting: why it matters, when to do it, how - -Reports are the **only non-circular signal** in the scoring system. Probe-based measurements predict their own inputs (the regularity component is derived from probe uptime, so "score predicts probe success" is trivially true). Your reports — *"I paid target X, it worked / it failed"* — are the ground truth that keeps the network's trust graph honest. - -### Why you personally should care - -- **Preimage-verified reports earn 2× weight**, tightening the score of every agent you transact with — so the next time *you* query a target, the score is more accurate because *you* (and people like you) fed it outcomes. -- Reports cost **nothing** (no quota consumed, no on-chain fee, one HTTP round-trip). -- The report endpoint is **free even without an API key** as long as you submit with an L402 token that already queried the target. - -### Two ways to report - -**Automatic (recommended)** — wrap your payment in `transact()`: - -```typescript -const result = await client.transact(target, caller, async () => { - const p = await myWallet.sendPayment(invoice); - return { success: p.ok, preimage: p.preimage, paymentHash: p.paymentHash }; -}); -``` - -The SDK submits the report for you. If submission fails (auth, rate limit), the payment outcome is preserved in `result.paid` and `result.report` is `null` — you can retry later via the manual path. +## Wallet drivers -**Manual** — any time after querying a target: +Import from the `@satrank/sdk/wallet` subpath: ```typescript -await client.report({ - target: '', - reporter: '', - outcome: 'success', // or 'failure' | 'timeout' - preimage: '', // optional — enables 2× weight - paymentHash: '', // required alongside preimage - memo: 'payment completed in 2.3s', // optional, free text -}); +import { LndWallet, NwcWallet, parseNwcUri, LnurlWallet } from '@satrank/sdk/wallet'; ``` -### Auth rules — which token can report on which target? - -One of these must be true: - -1. **You're using an `X-API-Key`** (partner-tier access) — report on anything. -2. **You're using an L402 token** (`Authorization: L402 :` or `L402 deposit:`) that has **already queried** the target through *any* paid endpoint: `/api/decide`, `/api/verdicts`, `/api/agent/:hash/verdict`, `/api/profile/:id`, or `/api/best-route`. The token does not need remaining balance to submit a report, only to be non-exhausted. - -If the token you hold never hit any of those endpoints for the target you want to report on, the simplest fix is to query the target once (for example `client.getProfile(target)`) and then resubmit the report — the query binds the token to the target for future reports. - -### When to report — a simple rule +| Driver | Transport | Notes | +|---------------|-------------------------------------------|-----------------------------------------------------------| +| `LndWallet` | LND REST, macaroon auth | Host your own node. | +| `NwcWallet` | Nostr Wallet Connect (NIP-47, encrypted) | Connects to any NWC-compatible wallet over Nostr relays. | +| `LnurlWallet` | LNURL-pay / LNbits-style HTTP | Simplest drop-in for custodial setups. | -Report **every paid interaction** the moment it completes: +Any object that implements `{ payInvoice(bolt11, maxFeeSats), isAvailable() }` works — the `Wallet` interface is intentionally narrow. -- `outcome: 'success'` — the payment settled. -- `outcome: 'failure'` — the invoice was rejected, the channel couldn't route, the HTL timed out. -- `outcome: 'timeout'` — you gave up before getting a terminal answer. - -Always include `preimage` + `paymentHash` when you have them. The 2× weight is the single biggest lever on report influence. - -### Rate limits & dedup - -- **Rate limit**: 20 reports/minute per reporter. Soft cap; a busy agent that legitimately transacts this fast should open an issue. -- **Dedup**: one report per `(reporter, target)` per hour. Re-submitting within the window returns `409 Conflict` and does not overwrite the original. - -### Reporter badge: how the server weights you - -Your reporter weight on each `/api/report` submission is derived server-side from two inputs: your own agent score (fetched via `getProfile(yourHash)`) and a *badge tier* inferred from your recent verified-report count. The badge tier is not yet exposed as a standalone field on `ProfileResponse` — it's folded into the `weight` returned on every `ReportResponse`: +## NLP helper ```typescript -const r = await client.report({ target, reporter, outcome: 'success', preimage, paymentHash }); -r.verified // true when paymentHash + preimage validate -r.weight // effective weight the server applied (score × tier × 2× preimage bonus) -``` - -Tier thresholds (applied server-side): - -| Tier | Threshold | Meaning | -|------|-----------|---------| -| `novice` | 0 verified reports in the last 30 days | Base weight | -| `contributor` | ≥ 5 verified reports | Weighted upward | -| `trusted` | ≥ 20 verified reports | Full weight, counts toward sovereign PageRank peer-trust | - -To track your own progress: sum `/api/report` responses locally, or call `getProfile(yourHash)` and read `reports.total` / `reports.successRate` — these are reports you *received*, not submitted. A dedicated submitter-stats field may ship in a future `ProfileResponse` revision; the badge effect is already active in scoring. +import { parseIntent } from '@satrank/sdk/nlp'; -## Deposit: Buy Bulk Balance - -The deposit flow is a two-step process. SatRank generates a Lightning invoice, you pay it with your wallet, then you verify the payment to activate your balance. - -```typescript -// Step 1: Request an invoice (free endpoint, no auth needed) -const invoice = await client.deposit(500); // 500 sats = 500 requests -console.log(invoice.invoice); // "lnbc5u1..." — pay this with your wallet -console.log(invoice.paymentHash); // "a1b2c3..." — you'll need this in step 3 - -// Step 2: Pay the invoice with your Lightning wallet (out-of-band) -// Use NWC, Phoenix, LND, or any wallet. SatRank never touches your funds. -const preimage = await myWallet.pay(invoice.invoice); - -// Step 3: Verify payment and activate balance -const result = await client.verifyDeposit(invoice.paymentHash, preimage); -console.log(result.token); // "L402 deposit:be7740a4..." — your auth token -console.log(result.balance); // 500 - -// Step 4: Use the token on all paid endpoints -const authedClient = new SatRankClient('https://satrank.dev', { - headers: { 'Authorization': result.token }, -}); +const intent = parseIntent('find me a cheap weather API for Paris under 50 sats'); +// { category: 'data/weather', keywords: ['paris'], budget_sats: 50 } ``` -### Cost vs. value +EN-only in 1.0. Passes the result straight into `sr.fulfill({ intent, budget_sats })`. -| Volume | Daily oracle cost | Break-even | -|--------|-------------------|------------| -| 100 payments/day | ~300 sats | 1 avoided failure | -| 1,000 payments/day | ~3,000 sats | 1 avoided failure | -| 10,000 payments/day | ~30,000 sats | 1 avoided failure | +## Options -A failed Lightning payment costs more than the oracle fee: routing fees are lost, the HTLC timeout locks capital for 30-60 seconds, and the retry adds latency. The oracle pays for itself by avoiding a single bad payment per day. +### `new SatRank(opts)` -## Positional Pathfinding +| Option | Type | Default | Description | +|-----------------------|-------------------------|--------------|------------------------------------------------------| +| `apiBase` | `string` | — | Required. e.g. `https://satrank.dev` | +| `wallet` | `Wallet` | — | Required for paid candidates. See drivers above. | +| `caller` | `string` | — | Free-form identifier piped into `/api/intent` logs. | +| `depositToken` | `string` | — | `L402 deposit:` token. Required for `auto_report`. | +| `request_timeout_ms` | `number` | `10000` | Per-API-call timeout. | +| `fetch` | `typeof fetch` | `globalThis.fetch` | DI point for Node <18 / tests. | -Most agents don't run their own LND node. They pay via wallet providers and don't know their position in the Lightning graph. Pass `walletProvider` to get pathfinding computed from your provider's hub node: - -```typescript -const decision = await client.decide({ - target: '', - caller: '', - walletProvider: 'phoenix', // pathfinding from ACINQ's node - serviceUrl: 'https://api.example.com', // HTTP health check -}); -// decision.pathfinding.sourceNode = "03864ef025fd..." -// decision.pathfinding.hops = 1 (instead of 4-5 from SatRank) -// decision.serviceHealth = { status: "healthy", servicePriceSats: 1 } -``` +### `sr.fulfill(opts)` -Supported providers: `phoenix`, `wos`, `strike`, `blink`, `breez`, `zeus`, `coinos`, `cashapp`. +| Option | Type | Default | Description | +|-----------------|---------------------------------------|------------------|-------------------------------------------------------| +| `intent` | `Intent` (`{category, keywords?, budget_sats?, max_latency_ms?}`) | — | Required. | +| `budget_sats` | `number` | — | Hard cap on total sats across all attempts. | +| `timeout_ms` | `number` | `30000` | Wall-clock cap across candidates. | +| `retry_policy` | `'next_candidate' \| 'none'` | `'next_candidate'` | Whether to try subsequent candidates on failure. | +| `auto_report` | `boolean` | `true` | Auto-submit outcome to `/api/report` (needs `depositToken`). | +| `caller` | `string` | constructor | Per-call override. | +| `limit` | `number` | `5` | Max candidates from `/api/intent` (server caps at 20).| +| `request` | `FulfillRequest` (`{method?, path?, query?, headers?, body?}`) | `GET` | Shape the downstream call. | +| `max_fee_sats` | `number` | `10` | Per-candidate fee cap handed to the wallet driver. | -Alternatively, pass `callerNodePubkey` with any Lightning pubkey to use as the pathfinding source. If both are provided, `callerNodePubkey` takes priority. +Returns a `FulfillResult` with `success`, `response_body`, `cost_sats`, `preimage`, `endpoint_used`, `candidates_tried[]`, and on failure a typed `error.code`. -## Agent Workflow: Screen, Route, Decide - -The recommended three-step pattern for autonomous agents evaluating payment candidates: screen many with batch verdicts, find the best route, then decide on the winner. 3 requests, under 1 second for 100 candidates. +## Error handling ```typescript -import { SatRankClient, SatRankError } from '@satrank/sdk'; - -const client = new SatRankClient('https://satrank.dev', { - headers: { 'Authorization': 'L402 :' }, -}); - -// Step 1: Screen up to 100 candidates (1 request from L402 balance, ~250ms) -const candidateHashes: string[] = [/* ...up to 100 SHA-256 hashes */]; - -const response = await fetch('https://satrank.dev/api/verdicts', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'L402 :', - }, - body: JSON.stringify({ hashes: candidateHashes }), -}); -const { verdicts } = await response.json(); - -// Filter to SAFE nodes -const safeNodes = verdicts - .filter((v: { verdict: string }) => v.verdict === 'SAFE') - .map((v: { hash: string }) => v.hash); - -// Step 2: Find best route among SAFE candidates (1 request, ~100ms) -const routeResult = await client.bestRoute({ - targets: safeNodes, - caller: '', - amountSats: 50000, -}); -const topCandidate = routeResult.candidates[0]; // top by composite rank - -// Step 3: Decide on the winner (1 request, ~150ms) -const decision = await client.decide({ - target: topCandidate.target, - caller: '', -}); +import { + SatRankError, + BalanceExhaustedError, + PaymentRequiredError, + PaymentPendingError, + DuplicateReportError, + RateLimitedError, + TimeoutError, + NetworkError, + ServiceUnavailableError, + UnauthorizedError, + ValidationSatRankError, + WalletError, +} from '@satrank/sdk'; -if (decision.go) { - // Pay with confidence - // targetFeeStability: fee stability of the target node (0 = volatile, 1 = stable, < 0.3 = warning) - // maxRoutableAmount: highest amount with a known route (compare with your payment) - console.log(`GO: rate=${decision.successRate}, feeVol=${decision.targetFeeStability}, maxRoute=${decision.maxRoutableAmount}`); - await myWallet.pay(topCandidate.target, amountSats); +try { + await sr.fulfill({ intent, budget_sats: 100 }); +} catch (err) { + if (err instanceof RateLimitedError || err instanceof TimeoutError) { + // retryable + } else if (err instanceof SatRankError) { + console.error(err.statusCode, err.code, err.message); + } } - -// Concrete numbers: -// - 100 candidates screened in ~250ms, 1 request -// - Best route found in ~100ms, 1 request -// - 1 decision in ~150ms, 1 request -// - Total: 3 requests from L402 balance, ~500ms -// - Pricing: 1 sat = 1 request (L402: 21 sats/21 reqs, or deposit: up to 10,000) -``` - -## Monitoring: Watch for Verdict Changes - -Two options for monitoring your targets. **Nostr is recommended** (real-time, free, decentralized). HTTP polling is the fallback. - -### Option 1: Nostr NIP-85 Subscription (recommended) - -SatRank publishes NIP-85 kind 30382 events every 30 minutes (delta-only — unchanged agents are skipped). Subscribe via any Nostr relay to get real-time push notifications when a score changes. - -```typescript -import { SatRankClient } from '@satrank/sdk'; - -const client = new SatRankClient('https://satrank.dev'); - -// Subscribe to score changes for specific Lightning nodes. -// By default, only events created AFTER subscription are delivered -// (the Nostr filter includes `since: now` to skip historical events). -const unsubscribe = client.watchNostr( - [ - '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', // ACINQ - '026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2', // Boltz - ], - (event) => { - console.log(`${event.alias}: score=${event.score} verdict=${event.verdict} reachable=${event.reachable}`); - // event.components = { volume: 100, reputation: 75, ... } - }, - { includeHistory: false }, // default -); - -// To also receive historical events (useful for backfill): -const unsubAll = client.watchNostr(targets, onEvent, { includeHistory: true }); - -// Or start from a specific timestamp: -const unsubSince = client.watchNostr(targets, onEvent, { since: 1776000000 }); - -// Later: stop watching -unsubscribe(); -``` - -Under the hood, the SDK opens WebSocket connections to 3 Nostr relays and sends: -```json -["REQ", "satrank-xxx", { - "kinds": [30382], - "authors": ["5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4"], - "#d": ["03864ef...", "026165..."] -}] ``` -Any Nostr client (nak, nostr-tools, nostcat) can do the same without the SDK. - -### Option 2: HTTP Polling (fallback) +`SatRankError.isRetryable()` returns true for 429/503/504/network/timeout. Most `fulfill()` failures do **not** throw — they surface in `result.error` so the candidate loop can continue cleanly. -Poll `GET /api/watchlist` for changes. Free endpoint, no L402 required. +## Documentation -**Staleness guarantees:** -- Max staleness: **60 seconds** after a verdict change (cache TTL). -- Responses within a 5-minute window on the same target set share a cache - populated by the first poll. `meta.effectiveSince` tells you the `since` - actually used for the DB query — you may receive changes older than your - requested `since` (always a superset). -- To advance through time, use `meta.queriedAt` as the `since` for your next - poll. Dedupe received changes by `changedAt > your_last_seen_ts`. - - -```typescript -const unsubscribe = client.watchPoll( - ['hash1...', 'hash2...'], // SHA-256 hashes (max 50) - { intervalMs: 300_000 }, // poll every 5 minutes - (changes) => { - for (const c of changes) { - console.log(`${c.alias}: ${c.previousScore} → ${c.score} (${c.verdict})`); - } - }, -); - -// Later: stop polling -unsubscribe(); -``` - -Or call `getWatchlist()` directly for one-shot queries: - -```typescript -const result = await client.getWatchlist( - ['hash1...', 'hash2...'], - Math.floor(Date.now() / 1000) - 3600, // changes in the last hour -); -console.log(`${result.meta.changed} targets changed`); -``` +- [Quickstart (TypeScript)](../docs/sdk/quickstart-ts.md) +- [Wallet drivers](../docs/sdk/wallet-drivers.md) +- [NLP helper](../docs/sdk/nlp-helper.md) +- [Migration 0.2.x → 1.0](../docs/sdk/migration-0.2-to-1.0.md) ## License diff --git a/sdk/package.json b/sdk/package.json index 2c83fe7..cd55c2a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,7 +1,7 @@ { "name": "@satrank/sdk", - "version": "1.0.0-rc.1", - "description": "SatRank SDK 1.0 — sr.fulfill() for AI agents on Bitcoin Lightning", + "version": "1.0.0", + "description": "SatRank SDK — sr.fulfill() for autonomous agents on Bitcoin Lightning", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { diff --git a/sdk/src/client/apiClient.ts b/sdk/src/client/apiClient.ts index 8106f64..6e2db2a 100644 --- a/sdk/src/client/apiClient.ts +++ b/sdk/src/client/apiClient.ts @@ -59,12 +59,6 @@ export class ApiClient { }); } - async getAgentVerdict(publicKeyHash: string): Promise { - return this.request('GET', `/api/agent/${publicKeyHash}/verdict`, undefined, { - requireAuth: true, - }); - } - private async request( method: 'GET' | 'POST', path: string, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 0d4143d..f6989fb 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -40,7 +40,7 @@ export interface BayesianBlock { export interface AdvisoryBlock { advisory_level: 'green' | 'yellow' | 'orange' | 'red'; risk_score: number; - recommendation: 'proceed' | 'proceed_with_caution' | 'avoid'; + recommendation: 'proceed' | 'proceed_with_caution' | 'consider_alternative' | 'avoid'; advisories: Array<{ code: string; level: 'info' | 'warning' | 'critical'; From 659d665fb9b4696634ffe50d8cb9d064b86c8ae1 Mon Sep 17 00:00:00 2001 From: Romain Orsoni Date: Wed, 22 Apr 2026 08:51:01 +0200 Subject: [PATCH 2/4] chore(sdk-1.0): align SDK licenses to MIT, bump Python classifier to Stable, fix keyword drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-publish adjustments for SatRank SDK 1.0.0 GA. License — both SDKs to MIT (client-side permissive, max adoption) - sdk/package.json: "license": "AGPL-3.0" -> "MIT" - sdk/README.md: license section -> MIT - sdk/LICENSE: new MIT file (copyright 2026 Romain Orsoni / SatRank) - sdk/package.json "files": add "LICENSE" to the npm publish list - python-sdk/LICENSE: new MIT file (matches existing pyproject.toml license = { text = "MIT" }) Python metadata - classifiers: "Development Status :: 4 - Beta" -> "5 - Production/Stable" (coherent with 1.0.0 GA) - keywords: "ai-agents" -> "autonomous-agents" (narrative consistency with the TS SDK and the rest of the Phase 6.1 wording) Rationale - MongoDB / Elastic pattern: server core stays AGPL-3.0 (protects the SatRank oracle backend); client SDKs are MIT (removes friction for agent developers). The economic protection via L402 on paid endpoints is orthogonal and unchanged. Artifacts rebuilt (not committed — matches prior policy) - sdk/satrank-sdk-1.0.0.tgz: 41.0 kB, 59 files, bundles LICENSE + README - python-sdk/dist/satrank-1.0.0-py3-none-any.whl + .tar.gz: LICENSE auto-included by setuptools in dist-info/licenses/ - Stale python-sdk/dist/satrank-1.0.0rc1.* removed during clean rebuild. PUBLISH GATE remains closed. No npm publish, no twine upload, no gh release, no git tag. Ready for manual publish per docs/phase-6.1/RELEASE-NOTES-DRAFT.md once validated. --- python-sdk/LICENSE | 21 ++++++++++++++++++ .../dist/satrank-1.0.0rc1-py3-none-any.whl | Bin 25836 -> 0 bytes python-sdk/dist/satrank-1.0.0rc1.tar.gz | Bin 28443 -> 0 bytes python-sdk/pyproject.toml | 4 ++-- sdk/LICENSE | 21 ++++++++++++++++++ sdk/README.md | 2 +- sdk/package.json | 5 +++-- 7 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 python-sdk/LICENSE delete mode 100644 python-sdk/dist/satrank-1.0.0rc1-py3-none-any.whl delete mode 100644 python-sdk/dist/satrank-1.0.0rc1.tar.gz create mode 100644 sdk/LICENSE diff --git a/python-sdk/LICENSE b/python-sdk/LICENSE new file mode 100644 index 0000000..900c225 --- /dev/null +++ b/python-sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Romain Orsoni / SatRank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/python-sdk/dist/satrank-1.0.0rc1-py3-none-any.whl b/python-sdk/dist/satrank-1.0.0rc1-py3-none-any.whl deleted file mode 100644 index 2d68d83b01a30bb4e8eab3c6ce67181d1ad0fec0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25836 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 diff --git a/python-sdk/pyproject.toml b/python-sdk/pyproject.toml index 6f2e71d..1ea2553 100644 --- a/python-sdk/pyproject.toml +++ b/python-sdk/pyproject.toml @@ -10,9 +10,9 @@ readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "SatRank" }] -keywords = ["lightning", "bitcoin", "l402", "ai-agents", "nostr"] +keywords = ["lightning", "bitcoin", "l402", "autonomous-agents", "nostr"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", diff --git a/sdk/LICENSE b/sdk/LICENSE new file mode 100644 index 0000000..900c225 --- /dev/null +++ b/sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Romain Orsoni / SatRank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/README.md b/sdk/README.md index ad7f434..6e2484a 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -152,4 +152,4 @@ try { ## License -AGPL-3.0 +MIT diff --git a/sdk/package.json b/sdk/package.json index cd55c2a..d152ff8 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -20,7 +20,8 @@ }, "files": [ "dist/", - "README.md" + "README.md", + "LICENSE" ], "scripts": { "build": "tsc -p tsconfig.build.json", @@ -47,7 +48,7 @@ "nwc", "lnd" ], - "license": "AGPL-3.0", + "license": "MIT", "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.7.3", From 05016dfb15307512890c9df49825f75e230af967 Mon Sep 17 00:00:00 2001 From: Romain Orsoni Date: Tue, 21 Apr 2026 23:56:28 +0200 Subject: [PATCH 3/4] ci: wire postgres 16 service container for npm test (Phase 12C #1) Adds a postgres:16-alpine service container to the test job with healthcheck so the Node test harness's globalSetup can connect and bootstrap the template DB. DATABASE_URL env var matches the default that src/tests/helpers/testDatabase.ts falls back to. Fixes the CI failure pattern observed on PR #13: Error: connect ECONNREFUSED 127.0.0.1:5432 at Object.setup (src/tests/helpers/globalSetup.ts:25:22) Credentials mirror the satrank/satrank/satrank default used locally so we do not diverge test expectations between dev and CI. GitHub Actions waits for the service healthcheck to pass before starting the job steps, so no external wait-for-it script is needed. --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a571c83..abc760f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,22 @@ on: jobs: test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: satrank + POSTGRES_PASSWORD: satrank + POSTGRES_DB: satrank + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U satrank -d satrank" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql://satrank:satrank@localhost:5432/satrank steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 82e8e93acf0978ca9b2fedf11fc4d04dc8bda425 Mon Sep 17 00:00:00 2001 From: Romain Orsoni Date: Wed, 22 Apr 2026 09:03:01 +0200 Subject: [PATCH 4/4] chore(sdk): normalize package.json repository.url --- sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/package.json b/sdk/package.json index d152ff8..19cd510 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -34,7 +34,7 @@ "bugs": "https://github.com/proofoftrust21/satrank/issues", "repository": { "type": "git", - "url": "https://github.com/proofoftrust21/satrank", + "url": "git+https://github.com/proofoftrust21/satrank.git", "directory": "sdk" }, "keywords": [