Metadata-Version: 2.4
Name: hoodly
Version: 0.1.2
Summary: Proof of task for robots — anchor completed work on Robinhood Chain
Author: Hoodly
License: Proprietary
Project-URL: Homepage, https://www.hoodly.fun
Project-URL: Documentation, https://www.hoodly.fun/docs/sdk
Project-URL: Download, https://www.hoodly.fun/developers
Keywords: robotics,ros2,audit-trail,blockchain,compliance,proof
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: mypy>=1.5; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"

# hoodly — proof of task for robots

Turn every task your robot finishes into a tamper-proof receipt anchored on
Robinhood Chain. No wallet on the machine, no chain tooling, no key management —
the robot makes ordinary HTTPS calls with an API key.

```bash
pip install hoodly
```

**Zero runtime dependencies.** Everything runs on the standard library, so it
installs cleanly on an industrial PC, a Jetson, or inside a ROS 2 workspace
without dragging in a dependency resolver conflict.

## Quick start

Create a robot in the [dashboard](https://hoodly.fun/dashboard) to get an API
key, then:

```python
from hoodly import HoodlyClient

client = HoodlyClient(api_key="hdly_…")  # or set HOODLY_API_KEY

proof = client.prove(
    "Inspect conveyor B4",
    {"defects_found": 0, "frames_checked": 1420, "operator": "shift-2"},
)

print(f"anchored in block {proof.block_number}")
print(f"share it: https://hoodly.fun{proof.share_url}")
```

Anyone can now re-check that proof without an account, and without asking you
for access.

## Claim first, prove later

`prove()` is a shortcut for work that is already done. When the robot should
record that it *started* — before it knows the outcome — split the call. The
claim timestamp then becomes part of the record.

```python
task = client.create_task("Inspect conveyor B4")

result = run_inspection()  # however long this takes

proof = client.complete_task(
    task.id,
    {
        "defects_found": result.defects,
        "duration_s": result.duration,
    },
)
```

## The one thing you must not do by hand

Anchoring costs burned $HOODLY and prepaid gas. If a completion request times
out, the transaction may already be on the chain — so a naive
`for attempt in range(3): retry()` can anchor the same task twice and pay twice
for one piece of work.

`complete_task()` never retries the submission. When it gets no usable answer it
*reconciles*: it asks what actually happened to the task and reports that.

```python
from hoodly import AnchorUnconfirmedError, InsufficientCreditError

try:
    proof = client.complete_task(task.id, evidence)
except AnchorUnconfirmedError as exc:
    # Broadcast, not confirmed in time. Do NOT resubmit.
    log.warning("anchor in flight: %s", exc.explorer_url)
    proof = client.wait_for_anchor(task.id, timeout=600)
except InsufficientCreditError as exc:
    log.error("out of credit: %s", exc.balances)
```

Every failure mode has its own exception type, because a robot has to react
differently to each:

| Exception | Meaning | What to do |
| --- | --- | --- |
| `AnchorUnconfirmedError` | Transaction broadcast, receipt not seen | Poll, never resubmit |
| `InsufficientCreditError` | No quota or no gas; nothing was sent | Top up, then retry |
| `TaskConflictError` | Task no longer accepts a proof | Investigate; do not resend |
| `AnchoringFailedError` | Nothing reached the chain, hold released | Safe to submit as a new task |
| `RateLimitError` | Too many calls for this robot | Back off |
| `TransportError` | No HTTP response at all | Handled internally by reconciliation |

## Robots that go offline

Mobile robots lose connectivity constantly. `OfflineQueue` writes evidence to a
local SQLite file first and uploads it when the network returns. The file
survives a reboot and a power cut.

```python
from hoodly import HoodlyClient
from hoodly.queue import OfflineQueue

client = HoodlyClient()
queue = OfflineQueue("/var/lib/hoodly/queue.db")

# On the robot, whenever a job finishes — never blocks on the network:
queue.enqueue("Inspect conveyor B4", {"defects_found": 0})

# On a timer, or when connectivity comes back:
report = queue.flush(client)
print(f"anchored {report.anchored}, {report.still_pending} still queued")
```

Two properties worth knowing about:

**A crash cannot cost you a duplicate task.** The task id is persisted the
moment a claim succeeds, so resuming completes *that* task rather than claiming a
second one.

**The block timestamp is when the proof was anchored, not when the work
happened.** For queued work those differ, sometimes by hours. The queue
therefore records the real moment inside the evidence as `hoodly_occurred_at` —
the chain timestamps the record, the evidence timestamps the event. Say so in
your own documentation too; an auditor will ask.

`flush()` is bounded in time so it is safe to call from a control loop or a ROS 2
timer callback. It stops early on conditions the next entry would also hit — no
network, no credit, rate limited — and preserves order, so the anchored sequence
matches the real one.

## Verifying a proof

Verification needs no API key. That is the point: your customer, your auditor and
your insurer can all check a proof without going through you.

```python
result = client.verify(proof.proof_hash)

if result and result.verified:
    print("still intact")
else:
    print("failed checks:", result.failed_checks if result else "not found")
```

Read `checks`, not just `verified`. Each check is broken out — whether the
evidence still hashes to the anchored hash, whether the block's calldata carries
that hash, whether the sender was Hoodly's anchor wallet — because a summary is
exactly what someone auditing you should not have to trust.

## Telemetry

Operational snapshots for the dashboard. Not anchored, not billed, not part of
any proof.

```python
client.send_telemetry(battery_pct=82.5, state="charging", metrics={"temp_c": 41.2})
```

## Configuration

| Argument | Environment variable | Default |
| --- | --- | --- |
| `api_key` | `HOODLY_API_KEY` | required |
| `base_url` | `HOODLY_BASE_URL` | `https://www.hoodly.fun` |
| `timeout` | — | `90.0` seconds |
| `max_retries` | — | `3` (retry-safe calls only) |

The timeout defaults high because completion waits for a chain receipt
server-side.

## ROS 2

See the `hoodly_ros` package: a ROS 2 node that exposes proving as an action and
can assemble evidence from `/tf`, `/diagnostics` and Nav2 results without any
application code. It builds on this library.

## Links

- [Documentation](https://www.hoodly.fun/docs)
- [Verification spec](https://www.hoodly.fun/docs/verification)
- [Proof explorer](https://www.hoodly.fun/explorer)
- [Audit trails and EU regulation](https://www.hoodly.fun/compliance)

Proprietary. See the repository LICENSE for terms.
