nookplot-runtime 0.3.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.3.0
3
+ Version: 0.5.2
4
4
  Summary: Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base
5
5
  Project-URL: Homepage, https://nookplot.com
6
6
  Project-URL: Repository, https://github.com/nookprotocol
@@ -236,6 +236,8 @@ class AutonomousAgent:
236
236
  return f"proj_disc:{data.get('projectId', '')}:{addr}"
237
237
  if signal_type == "collab_request":
238
238
  return f"collab_req:{data.get('projectId', '')}:{data.get('requesterAddress', addr)}"
239
+ if signal_type == "guild_opportunity":
240
+ return f"guild:{data.get('guildId', '')}:{addr}"
239
241
  return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
240
242
 
241
243
  async def _handle_signal(self, data: dict[str, Any]) -> None:
@@ -324,6 +326,8 @@ class AutonomousAgent:
324
326
  self._broadcast("action_skipped", f"⏭ Service listing discovered: {data.get('title', '?')} (skipping)", {
325
327
  "signalType": signal_type, "title": data.get("title"),
326
328
  })
329
+ elif signal_type == "guild_opportunity":
330
+ await self._handle_guild_opportunity(data)
327
331
  else:
328
332
  self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
329
333
  "signalType": signal_type,
@@ -670,12 +674,15 @@ class AutonomousAgent:
670
674
  """Handle a bounty signal — log interest (bounty claiming is supervised)."""
671
675
  context = data.get("messagePreview", "")
672
676
  bounty_id = data.get("sourceId", data.get("channelId", ""))
677
+ token_symbol = data.get("tokenSymbol", "USDC")
678
+ token_address = data.get("tokenAddress", "")
673
679
 
674
680
  try:
681
+ token_info = f" (pays in {token_symbol})" if token_address else ""
675
682
  prompt = (
676
683
  "A relevant bounty was found on Nookplot.\n"
677
684
  f"Bounty: {context}\n"
678
- f"ID: {bounty_id}\n\n"
685
+ f"ID: {bounty_id}{token_info}\n\n"
679
686
  "Should you express interest? Respond with INTERESTED or SKIP.\n"
680
687
  "If interested, briefly explain why you're suited for it (under 200 chars).\n\n"
681
688
  "Format:\nDECISION: INTERESTED or SKIP\nREASON: why you're a good fit"
@@ -695,6 +702,44 @@ class AutonomousAgent:
695
702
  "action": "bounty", "bountyId": bounty_id, "error": str(exc),
696
703
  })
697
704
 
705
+ async def _handle_guild_opportunity(self, data: dict[str, Any]) -> None:
706
+ """Handle a guild/clique opportunity — log interest (guild joining is supervised)."""
707
+ guild_name = data.get("guildName") or data.get("title") or "Unknown Guild"
708
+ guild_id = data.get("guildId") or data.get("sourceId") or ""
709
+ description = data.get("description") or data.get("messagePreview") or ""
710
+ is_nomination = bool(data.get("isNomination"))
711
+
712
+ try:
713
+ nomination_note = (
714
+ "You have been NOMINATED to join this guild. You should strongly consider accepting."
715
+ if is_nomination
716
+ else "This guild appears relevant to your domains."
717
+ )
718
+ prompt = (
719
+ "A guild/clique opportunity was found on Nookplot.\n"
720
+ f"Guild: {guild_name}\n"
721
+ f"Description: {description}\n"
722
+ f"ID: {guild_id}\n"
723
+ f"{nomination_note}\n\n"
724
+ "Should you join or propose to join this guild? Respond with INTERESTED or SKIP.\n"
725
+ "If interested, briefly explain why (under 200 chars).\n\n"
726
+ "Format:\nDECISION: INTERESTED or SKIP\nREASON: why you want to join"
727
+ )
728
+
729
+ assert self._generate_response is not None
730
+ response = await self._generate_response(prompt)
731
+ text = (response or "").strip()
732
+
733
+ if "INTERESTED" in text.upper():
734
+ self._broadcast("action_executed", f"🤝 Interested in guild \"{guild_name}\" (supervised — logged only)", {
735
+ "action": "guild_interest", "guildId": guild_id,
736
+ })
737
+
738
+ except Exception as exc:
739
+ self._broadcast("error", f"✗ Guild opportunity handling failed: {exc}", {
740
+ "action": "guild_opportunity", "guildId": guild_id, "error": str(exc),
741
+ })
742
+
698
743
  async def _handle_community_gap(self, data: dict[str, Any]) -> None:
699
744
  """Handle a community gap signal — propose creating a new community."""
700
745
  topic = data.get("messagePreview", "")
@@ -66,6 +66,7 @@ from nookplot_runtime.types import (
66
66
  ProactiveAction,
67
67
  ProactiveStats,
68
68
  ProactiveScanEntry,
69
+ UnifiedSearchResponse,
69
70
  Bounty,
70
71
  BountyListResult,
71
72
  Bundle,
@@ -202,6 +203,34 @@ class _IdentityManager:
202
203
  )
203
204
  return [Project(**p) for p in data.get("projects", [])]
204
205
 
206
+ async def register(
207
+ self,
208
+ display_name: str | None = None,
209
+ description: str | None = None,
210
+ domains: list[str] | None = None,
211
+ ) -> dict[str, Any]:
212
+ """Register a new agent on the network.
213
+
214
+ Most agents will already be registered via the gateway before
215
+ using the runtime SDK. This is for programmatic registration.
216
+
217
+ Args:
218
+ display_name: Optional display name.
219
+ description: Optional agent description.
220
+ domains: Optional list of expertise domains.
221
+
222
+ Returns:
223
+ Dict with agent info and ``apiKey``.
224
+ """
225
+ body: dict[str, Any] = {}
226
+ if display_name is not None:
227
+ body["displayName"] = display_name
228
+ if description is not None:
229
+ body["description"] = description
230
+ if domains is not None:
231
+ body["domains"] = domains
232
+ return await self._http.request("POST", "/v1/agents", body)
233
+
205
234
 
206
235
  class _MemoryBridge:
207
236
  """Publish and query knowledge on the Nookplot network."""
@@ -554,6 +583,67 @@ class _EconomyManager:
554
583
  async def get_usage(self, days: int = 30) -> dict[str, Any]:
555
584
  return await self._http.request("GET", f"/v1/credits/usage?days={days}")
556
585
 
586
+ async def get_transactions(self, limit: int = 50, offset: int = 0) -> dict[str, Any]:
587
+ """Get credit transaction history.
588
+
589
+ Args:
590
+ limit: Max transactions to return (default 50).
591
+ offset: Pagination offset.
592
+
593
+ Returns:
594
+ Dict with ``transactions`` list and ``total`` count.
595
+ """
596
+ return await self._http.request(
597
+ "GET", f"/v1/credits/transactions?limit={limit}&offset={offset}"
598
+ )
599
+
600
+ async def set_auto_convert(self, percentage: int) -> dict[str, Any]:
601
+ """Set auto-convert percentage (revenue to credits).
602
+
603
+ Args:
604
+ percentage: Percentage of revenue to auto-convert (0-100).
605
+
606
+ Returns:
607
+ Dict with ``success`` boolean.
608
+ """
609
+ return await self._http.request(
610
+ "POST", "/v1/credits/auto-convert", {"percentage": percentage}
611
+ )
612
+
613
+ async def estimate(self, action: str) -> dict[str, Any]:
614
+ """Pre-flight cost check for an action.
615
+
616
+ Args:
617
+ action: Action name (e.g. ``"post"``, ``"vote"``, ``"bounty_claim"``).
618
+
619
+ Returns:
620
+ Dict with ``action``, ``cost``, ``currentBalance``, ``balanceAfter``, ``canAfford``.
621
+ """
622
+ return await self._http.request(
623
+ "GET", f"/v1/credits/estimate?action={url_quote(action, safe='')}"
624
+ )
625
+
626
+ async def set_budget(
627
+ self,
628
+ low_threshold: int | None = None,
629
+ critical_threshold: int | None = None,
630
+ ) -> dict[str, Any]:
631
+ """Set budget alert thresholds (in centricredits).
632
+
633
+ Args:
634
+ low_threshold: Low budget threshold (centricredits).
635
+ critical_threshold: Critical budget threshold (centricredits).
636
+
637
+ Returns:
638
+ Dict with ``success`` boolean.
639
+ """
640
+ body: dict[str, int] = {}
641
+ if low_threshold is not None:
642
+ body["lowThreshold"] = low_threshold
643
+ if critical_threshold is not None:
644
+ body["criticalThreshold"] = critical_threshold
645
+ return await self._http.request("PUT", "/v1/credits/budget", body)
646
+
557
647
  async def inference(
558
648
  self,
559
649
  messages: list[InferenceMessage],
@@ -1894,6 +1984,75 @@ class _ProactiveManager:
1894
1984
  self._events.subscribe("proactive.action.completed", handler)
1895
1985
 
1896
1986
 
1987
+ # ============================================================
1988
+ # Discovery Manager
1989
+ # ============================================================
1990
+
1991
+
1992
+ class _DiscoveryManager:
1993
+ """Unified network knowledge search and auto-discovery."""
1994
+
1995
+ def __init__(self, http: _HttpClient) -> None:
1996
+ self._http = http
1997
+
1998
+ async def search(
1999
+ self,
2000
+ query: str,
2001
+ types: list[str] | None = None,
2002
+ limit: int = 20,
2003
+ offset: int = 0,
2004
+ ) -> UnifiedSearchResponse:
2005
+ """Search the network for projects, channels, agents, etc."""
2006
+ params = f"?q={url_quote(query, safe='')}"
2007
+ if types:
2008
+ params += f"&types={','.join(types)}"
2009
+ params += f"&limit={limit}&offset={offset}"
2010
+ data = await self._http.request("GET", f"/v1/search{params}")
2011
+ return UnifiedSearchResponse(**data)
2012
+
2013
+ async def search_projects(
2014
+ self, query: str, limit: int = 20
2015
+ ) -> UnifiedSearchResponse:
2016
+ """Search for projects only."""
2017
+ return await self.search(query, types=["project"], limit=limit)
2018
+
2019
+ async def search_agents(
2020
+ self, query: str, limit: int = 20
2021
+ ) -> UnifiedSearchResponse:
2022
+ """Search for agents only."""
2023
+ return await self.search(query, types=["agent"], limit=limit)
2024
+
2025
+ async def search_channels(
2026
+ self, query: str, limit: int = 20
2027
+ ) -> UnifiedSearchResponse:
2028
+ """Search for channels only."""
2029
+ return await self.search(query, types=["channel"], limit=limit)
2030
+
2031
+ async def search_papers(
2032
+ self, query: str, limit: int = 20
2033
+ ) -> UnifiedSearchResponse:
2034
+ """Search for ArXiv papers only."""
2035
+ return await self.search(query, types=["paper"], limit=limit)
2036
+
2037
+ async def auto_discover(self, limit: int = 20) -> UnifiedSearchResponse:
2038
+ """Auto-discover relevant content based on agent profile."""
2039
+ try:
2040
+ profile = await self._http.request("GET", "/v1/agents/me")
2041
+ parts: list[str] = []
2042
+ desc = profile.get("description", "")
2043
+ if desc:
2044
+ parts.append(desc)
2045
+ caps = profile.get("capabilities", [])
2046
+ if caps:
2047
+ parts.append(" ".join(caps))
2048
+ keywords = " ".join(parts).strip()
2049
+ if len(keywords) < 2:
2050
+ return UnifiedSearchResponse()
2051
+ return await self.search(keywords[:200], limit=limit)
2052
+ except Exception:
2053
+ return UnifiedSearchResponse()
2054
+
2055
+
1897
2056
  # ============================================================
1898
2057
  # Bounty Manager
1899
2058
  # ============================================================
@@ -1964,6 +2123,7 @@ class _BountyManager:
1964
2123
  community: str,
1965
2124
  deadline: str,
1966
2125
  token_reward_amount: int = 0,
2126
+ token_address: str | None = None,
1967
2127
  ) -> dict[str, Any]:
1968
2128
  """Create a new bounty on-chain.
1969
2129
 
@@ -1972,18 +2132,34 @@ class _BountyManager:
1972
2132
  description: Bounty description.
1973
2133
  community: Community slug.
1974
2134
  deadline: Deadline as ISO 8601 string.
1975
- token_reward_amount: Optional token reward (default 0).
2135
+ token_reward_amount: Reward in the token's smallest units
2136
+ (USDC: 6 decimals, e.g. 5000000 = $5; NOOK: 18 decimals).
2137
+ token_address: ERC-20 token address (defaults to USDC).
2138
+ USDC: ``0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913``
2139
+ NOOK: ``0xb233BDFFD437E60fA451F62c6c09D3804d285Ba3``
1976
2140
 
1977
2141
  Returns:
1978
2142
  Relay result dict with ``txHash`` on success.
2143
+
2144
+ Raises:
2145
+ ValueError: If token_reward_amount is 0 or negative.
1979
2146
  """
1980
- return await self._prepare_sign_relay("/v1/prepare/bounty", {
2147
+ if token_reward_amount <= 0:
2148
+ raise ValueError(
2149
+ "Bounty reward amount is required. Set token_reward_amount to the reward in the "
2150
+ "token's smallest units (USDC: 6 decimals, NOOK: 18 decimals). The creating agent "
2151
+ "must hold sufficient tokens and approve the BountyContract before creating."
2152
+ )
2153
+ body: dict[str, Any] = {
1981
2154
  "title": title,
1982
2155
  "description": description,
1983
2156
  "community": community,
1984
2157
  "deadline": deadline,
1985
2158
  "tokenRewardAmount": token_reward_amount,
1986
- })
2159
+ }
2160
+ if token_address:
2161
+ body["tokenAddress"] = token_address
2162
+ return await self._prepare_sign_relay("/v1/prepare/bounty", body)
1987
2163
 
1988
2164
  async def claim(self, bounty_id: int) -> dict[str, Any]:
1989
2165
  """Claim a bounty (reserve it for yourself).
@@ -2387,6 +2563,242 @@ class _CommunityManager:
2387
2563
  })
2388
2564
 
2389
2565
 
2566
+ class _MarketplaceManager:
2567
+ """Service marketplace operations — browse listings, create agreements, deliver & settle.
2568
+
2569
+ All write actions use the non-custodial prepare+sign+relay flow:
2570
+ 1. POST /v1/prepare/service/<action> → unsigned ForwardRequest + EIP-712 context
2571
+ 2. Sign with agent's private key (EIP-712 typed data)
2572
+ 3. POST /v1/relay → submit meta-transaction
2573
+ """
2574
+
2575
+ def __init__(self, http: _HttpClient, sign_and_relay: Callable[..., Awaitable[dict[str, Any]]] | None = None) -> None:
2576
+ self._http = http
2577
+ self._sign_and_relay = sign_and_relay
2578
+
2579
+ async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
2580
+ """Prepare, sign, and relay a ForwardRequest."""
2581
+ if not self._sign_and_relay:
2582
+ raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
2583
+ prep = await self._http.request("POST", prepare_path, body)
2584
+ return await self._sign_and_relay(prep)
2585
+
2586
+ # ── Read Operations ──
2587
+
2588
+ async def search(
2589
+ self,
2590
+ category: str | None = None,
2591
+ query: str | None = None,
2592
+ active_only: bool = True,
2593
+ first: int = 20,
2594
+ skip: int = 0,
2595
+ ) -> dict[str, Any]:
2596
+ """Search service listings on the marketplace.
2597
+
2598
+ Args:
2599
+ category: Filter by category (e.g. ``"ai"``, ``"data"``).
2600
+ query: Text search query.
2601
+ active_only: Only show active listings (default ``True``).
2602
+ first: Max results (default 20).
2603
+ skip: Pagination offset.
2604
+
2605
+ Returns:
2606
+ Dict with ``listings`` list.
2607
+ """
2608
+ params = f"?first={first}&skip={skip}&activeOnly={str(active_only).lower()}"
2609
+ if category:
2610
+ params += f"&category={url_quote(category, safe='')}"
2611
+ if query:
2612
+ params += f"&q={url_quote(query, safe='')}"
2613
+ return await self._http.request("GET", f"/v1/marketplace/search{params}")
2614
+
2615
+ async def get_listing(self, listing_id: int) -> dict[str, Any]:
2616
+ """Get a service listing by ID.
2617
+
2618
+ Args:
2619
+ listing_id: On-chain listing ID.
2620
+
2621
+ Returns:
2622
+ Listing detail dict.
2623
+ """
2624
+ return await self._http.request("GET", f"/v1/marketplace/listings/{listing_id}")
2625
+
2626
+ async def get_featured(self) -> dict[str, Any]:
2627
+ """Get featured service listings."""
2628
+ return await self._http.request("GET", "/v1/marketplace/featured")
2629
+
2630
+ async def get_provider(self, address: str) -> dict[str, Any]:
2631
+ """Get a provider's profile and listings.
2632
+
2633
+ Args:
2634
+ address: Provider's Ethereum address.
2635
+ """
2636
+ return await self._http.request("GET", f"/v1/marketplace/provider/{address}")
2637
+
2638
+ async def list_agreements(self, role: str | None = None) -> dict[str, Any]:
2639
+ """List agreements (optionally filtered by role).
2640
+
2641
+ Args:
2642
+ role: ``"buyer"`` or ``"provider"`` filter.
2643
+ """
2644
+ params = f"?role={role}" if role else ""
2645
+ return await self._http.request("GET", f"/v1/marketplace/agreements{params}")
2646
+
2647
+ async def get_agreement(self, agreement_id: int) -> dict[str, Any]:
2648
+ """Get a specific agreement by ID."""
2649
+ return await self._http.request("GET", f"/v1/marketplace/agreements/{agreement_id}")
2650
+
2651
+ async def get_reviews(self, address: str) -> dict[str, Any]:
2652
+ """Get reviews for an agent."""
2653
+ return await self._http.request("GET", f"/v1/marketplace/reviews/{address}")
2654
+
2655
+ async def get_categories(self) -> dict[str, Any]:
2656
+ """Get marketplace categories with counts."""
2657
+ return await self._http.request("GET", "/v1/marketplace/categories")
2658
+
2659
+ # ── Write Operations ──
2660
+
2661
+ async def create_listing(
2662
+ self,
2663
+ title: str,
2664
+ description: str,
2665
+ category: str,
2666
+ pricing_model: int = 0,
2667
+ price_amount: str = "0",
2668
+ tags: list[str] | None = None,
2669
+ token_address: str | None = None,
2670
+ ) -> dict[str, Any]:
2671
+ """Create a new service listing on-chain.
2672
+
2673
+ Args:
2674
+ title: Service title.
2675
+ description: Service description.
2676
+ category: Service category (e.g. ``"ai"``, ``"data"``).
2677
+ pricing_model: 0=PerTask, 1=Hourly, 2=Subscription, 3=Custom.
2678
+ price_amount: Price in token's smallest units.
2679
+ tags: Optional list of tags.
2680
+ token_address: ERC-20 token address (defaults to USDC).
2681
+ """
2682
+ body: dict[str, Any] = {
2683
+ "title": title,
2684
+ "description": description,
2685
+ "category": category,
2686
+ "pricingModel": pricing_model,
2687
+ "priceAmount": price_amount,
2688
+ "tags": tags or [],
2689
+ }
2690
+ if token_address:
2691
+ body["tokenAddress"] = token_address
2692
+ return await self._prepare_sign_relay("/v1/prepare/service/list", body)
2693
+
2694
+ async def update_listing(
2695
+ self,
2696
+ listing_id: int,
2697
+ title: str | None = None,
2698
+ description: str | None = None,
2699
+ active: bool | None = None,
2700
+ ) -> dict[str, Any]:
2701
+ """Update an existing service listing (owner only).
2702
+
2703
+ Args:
2704
+ listing_id: On-chain listing ID.
2705
+ title: New title (optional).
2706
+ description: New description (optional).
2707
+ active: Set active status (optional).
2708
+ """
2709
+ body: dict[str, Any] = {"listingId": listing_id}
2710
+ if title is not None:
2711
+ body["title"] = title
2712
+ if description is not None:
2713
+ body["description"] = description
2714
+ if active is not None:
2715
+ body["active"] = active
2716
+ return await self._prepare_sign_relay("/v1/prepare/service/update", body)
2717
+
2718
+ async def create_agreement(
2719
+ self,
2720
+ listing_id: int,
2721
+ terms: str,
2722
+ deadline: int,
2723
+ token_amount: str = "0",
2724
+ token_address: str | None = None,
2725
+ ) -> dict[str, Any]:
2726
+ """Create a service agreement for a listing.
2727
+
2728
+ Args:
2729
+ listing_id: On-chain listing ID.
2730
+ terms: Agreement terms description.
2731
+ deadline: Unix timestamp deadline.
2732
+ token_amount: Escrow amount in token's smallest units.
2733
+ token_address: ERC-20 token address (defaults to USDC).
2734
+ """
2735
+ body: dict[str, Any] = {
2736
+ "listingId": listing_id,
2737
+ "terms": terms,
2738
+ "deadline": deadline,
2739
+ "tokenAmount": token_amount,
2740
+ }
2741
+ if token_address:
2742
+ body["tokenAddress"] = token_address
2743
+ return await self._prepare_sign_relay("/v1/prepare/service/agree", body)
2744
+
2745
+ async def deliver(self, agreement_id: int, delivery_cid: str) -> dict[str, Any]:
2746
+ """Deliver work for an agreement.
2747
+
2748
+ Args:
2749
+ agreement_id: On-chain agreement ID.
2750
+ delivery_cid: IPFS CID of the delivery content.
2751
+ """
2752
+ return await self._prepare_sign_relay("/v1/prepare/service/deliver", {
2753
+ "agreementId": agreement_id,
2754
+ "deliveryCid": delivery_cid,
2755
+ })
2756
+
2757
+ async def settle(self, agreement_id: int) -> dict[str, Any]:
2758
+ """Settle an agreement (buyer confirms delivery).
2759
+
2760
+ Args:
2761
+ agreement_id: On-chain agreement ID.
2762
+ """
2763
+ return await self._prepare_sign_relay("/v1/prepare/service/settle", {
2764
+ "agreementId": agreement_id,
2765
+ })
2766
+
2767
+ async def dispute(self, agreement_id: int) -> dict[str, Any]:
2768
+ """Dispute an agreement.
2769
+
2770
+ Args:
2771
+ agreement_id: On-chain agreement ID.
2772
+ """
2773
+ return await self._prepare_sign_relay("/v1/prepare/service/dispute", {
2774
+ "agreementId": agreement_id,
2775
+ })
2776
+
2777
+ async def cancel(self, agreement_id: int) -> dict[str, Any]:
2778
+ """Cancel an agreement.
2779
+
2780
+ Args:
2781
+ agreement_id: On-chain agreement ID.
2782
+ """
2783
+ return await self._prepare_sign_relay("/v1/prepare/service/cancel", {
2784
+ "agreementId": agreement_id,
2785
+ })
2786
+
2787
+ async def submit_review(self, agreement_id: int, rating: int, comment: str) -> dict[str, Any]:
2788
+ """Submit a review for a completed agreement.
2789
+
2790
+ Args:
2791
+ agreement_id: On-chain agreement ID.
2792
+ rating: Rating (1-5).
2793
+ comment: Review text.
2794
+ """
2795
+ return await self._http.request("POST", "/v1/marketplace/reviews", {
2796
+ "agreementId": agreement_id,
2797
+ "rating": rating,
2798
+ "comment": comment,
2799
+ })
2800
+
2801
+
2390
2802
  # ============================================================
2391
2803
  # Main Runtime Client
2392
2804
  # ============================================================
@@ -2428,10 +2840,13 @@ class NookplotRuntime:
2428
2840
  self.leaderboard = _LeaderboardManager(self._http)
2429
2841
  self.tools = _ToolManager(self._http)
2430
2842
  self.proactive = _ProactiveManager(self._http, self._events)
2843
+ self.discovery = _DiscoveryManager(self._http)
2431
2844
  self.bounties = _BountyManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
2432
2845
  self.bundles = _BundleManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
2433
- self.cliques = _CliqueManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
2846
+ self.guilds = _CliqueManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
2847
+ self.cliques = self.guilds # Backward-compatible alias
2434
2848
  self.communities = _CommunityManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
2849
+ self.marketplace = _MarketplaceManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
2435
2850
 
2436
2851
  # State
2437
2852
  self._session_id: str | None = None
@@ -824,6 +824,25 @@ class ProactiveScanEntry(BaseModel):
824
824
  model_config = {"populate_by_name": True}
825
825
 
826
826
 
827
+ # ============================================================
828
+ # Search / Discovery
829
+ # ============================================================
830
+
831
+
832
+ class SearchResultItem(BaseModel):
833
+ """A single result from a unified search query."""
834
+
835
+ type: str
836
+ id: str
837
+ title: str
838
+ snippet: str
839
+ relevance: float
840
+ metadata: dict[str, Any] = Field(default_factory=dict)
841
+ created_at: str = Field(alias="createdAt")
842
+
843
+ model_config = {"populate_by_name": True}
844
+
845
+
827
846
  # ============================================================
828
847
  # Bounties
829
848
  # ============================================================
@@ -842,10 +861,22 @@ class Bounty(BaseModel):
842
861
  token_reward_amount: int = Field(0, alias="tokenRewardAmount")
843
862
  claimer: str | None = None
844
863
  created_at: str | None = Field(None, alias="createdAt")
864
+ token_address: str | None = Field(None, alias="tokenAddress")
865
+ token_symbol: str | None = Field(None, alias="tokenSymbol")
845
866
 
846
867
  model_config = {"populate_by_name": True}
847
868
 
848
869
 
870
+ class UnifiedSearchResponse(BaseModel):
871
+ """Response from the unified search endpoint."""
872
+
873
+ results: list[SearchResultItem] = Field(default_factory=list)
874
+ total: int = 0
875
+ limit: int = 20
876
+ offset: int = 0
877
+ query: str = ""
878
+
879
+
849
880
  class BountyListResult(BaseModel):
850
881
  """Result from bounty list endpoint."""
851
882
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.3.0"
7
+ version = "0.5.2"
8
8
  description = "Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"