Skip to content

Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-webots-shelf-inspection.v1 - #120

Open
EslaM-X wants to merge 33 commits into
fabricfoundation:boston-dynamics-atlas-tier-1from
EslaM-X:boston-dynamics-atlas-tier-1
Open

Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-webots-shelf-inspection.v1#120
EslaM-X wants to merge 33 commits into
fabricfoundation:boston-dynamics-atlas-tier-1from
EslaM-X:boston-dynamics-atlas-tier-1

Conversation

@EslaM-X

@EslaM-X EslaM-X commented Aug 19, 2026

Copy link
Copy Markdown

Boston Dynamics Atlas — Tier 1: Paid Multi-Step Shelf Inspection

Payment-gated, policy-driven shelf inspection on a free-standing Boston Dynamics
Atlas v4, executed through the RoboPay → Zenoh → simulator path and validated
across MuJoCo, PyBullet and Webots R2025a from one pinned robot description.

One paid Atlas action, end to end

One pass over one action, nothing edited together. The left pane is the real
exchange as it happens — skill and price discovered, the unpaid 402, the
EIP-3009 authorization signed with keccak256(action_id) as its nonce, the
202, the action on Zenoh, the correlated result, and 0.001 USDC settled
with its BaseScan link and the token's own confirmation that the authorization
was spent. The right pane is the episode that payment bought, rendered from
inside it: the frames are drawn from a callback on the scored run's own control
loop, so the two panes cannot be two different runs that happened to agree. The
settlement is
0x7fc1c1ff…b5b197,
block 45737187, and you can reach it without taking my word for the hash —
keccak256("atlas-inspect-1787242629") is the authorization nonce, which is
enough to find the AuthorizationUsed event on USDC and read the transfer off
it.

Atlas shelf inspection

The task close-up, in MuJoCo. Green spheres are the three targets at their
tolerance radius; the panel is live telemetry. The renderer refuses to write the
GIF unless the annotated episode reproduces the plain one exactly, so the picture
and the numbers cannot disagree.

TL;DR

  • 🤖 Boston Dynamics Atlas v4 — pinned, MIT-licensed, fetched not vendored
  • 🎯 3-target shelf inspection, each held before it counts
  • 🧠 Closed-loop damped-least-squares resolved-rate controller
  • 🧍 Free-standing — no weld, no clamp, no external support
  • 🧪 MuJoCo + PyBullet + Webots R2025a from one URDF
  • 📊 3/3 targets on all three engines
  • 💥 0 shelf collisions · 🧎 0 falls
  • 🔁 Sim-to-sim PASS, 3.20 mm spread across engines
  • 🔌 Payment-validated action over the real Zenoh transport, correlated by action_id
  • 💳 x402 safety: unpaid / invalid / malformed-hash / replay never reach Zenoh;
    a failed execution is accepted asynchronously and never settles
  • 🛡️ Live x402 facilitator refuses a forged authorization — proven, fails closed
  • 🔒 Durable idempotency — one actuation per key, across restarts
  • 🌐 The whole path, nothing stood in for: hosted Fabric relay → this repo's Go tunnel
    → live facilitator → Zenoh → Atlas 3/3 → relay status → settlement
  • 🔎 Robot, skill and price discovery through the relay — the payment is built from the
    price the relay quotes, not from a constant
  • 💰 One real paid action: live facilitator accepts → robot 3/3 → 0.001 USDC settled
  • ⛓️ Settlement bound to the action_id by keccak256(action_id), checkable on chain
  • 📦 Reproducible from a clean checkout

Tier 1 success criteria

Requirement How it is met Evidence
Simulator-only MuJoCo, PyBullet, Webots — no hardware anywhere in the repo validation-report.md
Approved simulator MuJoCo 3.11 primary; Webots R2025a and PyBullet cross-check sim2sim-validation.json
Policy / controller triggered State machine + DLS resolved-rate IK, re-solved from the measured pose each tick control_core.py
Not a replayed trajectory No recorded trajectory exists; a test feeds two controllers different measured poses and asserts different commands test_controller_is_closed_loop_not_a_replayed_trajectory
Not a built-in demo motion Controller and task written here; the upstream description contributes only the robot task.py
Simulator state metrics Per-target end-effector error, targets completed, pelvis height, collisions, duration mujoco-inspection-episode.json
Sim-to-Sim validation Same URDF, task and controller on three engines, bounded spread sim2sim-validation.json
Payment-validated action → Zenoh → simulator Verified action published on robot/tunnel/action, executed, answered on robot/tunnel/result tunnel-e2e-evidence.json
Real Go tunnel gates the action This repo's tunnel binary refuses unpaid (402) and forged (400) actions; neither reaches the simulator go-tunnel-e2e-evidence.json
Forged payment refused Live x402 facilitator returns invalid_exact_evm_signature; nothing executes tests/test_facilitator.py
One actuation per payment-validated action Durable idempotency store, surviving a restart tests/test_idempotency.py
Clean, easy-to-understand code no vendored binaries, one source of truth per concept this diff

End-to-end flow

Payment-validated action request
    ↓
x402 verification  ── rejected ─→ 402/400, never published, simulator untouched
    ↓
Zenoh: robot/tunnel/action
    ↓
Atlas bridge  (skill + parameter contract)
    ↓
inspect_shelf controller
    ↓
MuJoCo / PyBullet / Webots
    ↓
Zenoh: robot/tunnel/result
    ↓
correlation by action_id
    ↓
settlement — only on success

demo_tunnel.py runs exactly that over the real transport (Zenoh peer mode, no
router required). Every request in the walkthrough:

Request Verified Published to Zenoh Executed Settled
No payment no — 402 no no no
Wrong amount no — 400 no no no
Malformed txHash no — 400 no no no
Valid receipt † protocol checks pass yes 3/3 targets recorded, not on-chain
Replayed receipt † no — 400 no no no
Undeclared parameter yes yes refused by the bridge no
Forged authorization no — live facilitator no no no

† Replay answers differently on each path, because each one answers at a
different moment:

Path Replay response
Transport demo (demo_tunnel.py, Python client over Zenoh) 400
In-process relay (demo_e2e.py, answers synchronously) 409
Hosted Go tunnel (demo_fabric_e2e.py) 202, then DUPLICATE_ACTION on the status endpoint

These are three distinct transport and execution paths. All three reject a
replayed execution, and none actuates the robot a second time. The skill
contract names each case separately rather than leaving the difference to be
inferred from the code.

What the accepting row is, and where the real one lives. That receipt is
a synthetic hash, so the row shows the bridge's protocol checks passing —
amount, format, replay, declared parameters — and the action being executed and
answered. This walkthrough holds no wallet: it verifies no signature with
the facilitator and moves no value, and its artifact says so
(payment_verification: protocol_checks_only, settlement: eligible_not_on_chain). Its job is the transport and the refusal paths; the
forged authorization here is refused by the live x402 facilitator.

Accepted live payments are proven separately, twice, and both settle real USDC:
real-paid-run.json (facilitator isValid: true → 3/3 → 0.001 USDC) and
fabric-relay-e2e.json (the same, through the hosted relay). Nothing in this
section should be read as the profile's evidence for a real payment.

Two properties the demo asserts on itself: an unverified payment never reaches
Zenoh
, and every executed action is answered carrying the originating
action_id, robot_id, skill_id, params_hash and idempotency_key.

Through the hosted Fabric relay, with nothing stood in for

fabric-relay-e2e.json is the run where every component is the real one — the
hosted relay at api.fabric.foundation, this repository's Go tunnel dialled out
to it over WSS, the live facilitator, Zenoh, and MuJoCo:

client
  ↓  hosted Fabric relay   https://api.fabric.foundation/api/core
  ↓  Go tunnel (this repo, dialled out over WSS)
  ↓  x402 middleware → live facilitator
  ↓  Zenoh robot/tunnel/action → Atlas bridge → MuJoCo
  ↓  Zenoh robot/tunnel/result
  ↓  relay terminal status, correlated by action_id
  ↓  0.001 USDC settled on Base Sepolia
Step Result
Robot discovery GET /robots/{id}/skills200, robot connected
Skill discovery inspect_shelf, stop
Price discovery 0.001 USDC — read from the response, not assumed
Unpaid action 402 from the relay, carrying payment requirements
Quoted amount 1000 raw, matching the discovered price
Paid action 202 accepted immediately, carrying action_id and a status URL
Execution 3/3 targets
Terminal status succeeded, correlated by action_id, read back from the relay
Settlement after the result — 0xfd9eda75…1e6940, block 45714728
Token's own record authorizationState(...) = true — the authorization was spent
Binding on-chain nonce = keccak256("atlas-inspect-1787197727")

The price is discovered, not assumed. The payment is built from the amount
the relay quotes in its 402, and the run asserts that amount equals the price the
catalogue publishes — a profile whose advertised price had drifted from what its
tunnel charges would fail that check rather than pass quietly.

What this changes in the tunnel, stated in full. Three read-only endpoints
are added — GET /robot, GET /skills, GET /action/:action_id/status — and
POST /action is extended, not left alone: it keeps the existing x402
verification boundary, but now refuses an incomplete identity before publishing,
answers 202 Accepted with an action_id and a status URL instead of 200, and
defers settlement until the correlated result reports success. The stock x402
middleware is replaced by a gate that performs the same 402/verify half and
hands the settlement to that watcher. Calling this read-only would be
understating it, and the diff would say so.

The status endpoint is not synthesised: the tunnel subscribes to the same
robot/tunnel/result topic the simulator publishes on and stores what arrives,
keyed by action_id; an unanswered action reads pending, a failed one failed.
Merged robot-profile branches extend the tunnel the same way.

A failed action is not paid for, and the token says so. The same relay,
the same wallet, an action refused by the catalogue's declared bounds
(atlas-inspect-1787197752): the tunnel answers 202, nothing settles, and
authorizationState(payer, keccak256(action_id)) on the USDC contract is
false. "We recorded no transaction hash" is an absence of evidence; the
token's own map of spent authorization nonces is not, and because the nonce is
derived from the action id anyone can recompute it and ask USDC directly.

That on-chain artefact is one action. The invariant behind it — an unsuccessful
execution never settles — is held at the point where the decision is made, and
each case has a test that fails if the guarantee is removed:

Case Test
Execution reports failure TestAFailedEpisodeIsNeverSettled · test_valid_payment_failure_no_settlement
The robot never answers — execution times out TestASilentRobotIsNeverSettled
The skill raises mid-episode test_execution_exception_no_settlement
A payment is replayed test_replay_rejected · test_no_double_settle
Execution succeeds settles exactly onceTestSettlementFollowsSuccess · test_settle_on_success_only
The action never reaches Zenoh TestAFailedPublishLeavesNoWaiterBehind — 502, no waiter, no settlement

The Go cases run against the settlement gate itself, which is the component that
decides; the Python cases run against the bridge that produces the result it
decides on. Both suites run in CI on every commit.

This is enforced rather than hoped for, and without giving up the immediate
202 the tunnel contract promises. The stock x402 middleware settles as soon as
a protected route answers anything under 400, so a 202 would charge the payer
before the robot had run. The tunnel replaces it with a gate that keeps the
402/verify half unchanged — an unpaid request still gets 402 with the
advertised requirements, a payment the live facilitator rejects still never
reaches the robot — and hands a settlement callback to the handler instead of
settling. POST /action publishes, answers 202, and a background watcher
invokes that callback only when the correlated result reports success. A
failure or a silent robot leaves the authorization signed and unspent. Eight
tests in handlers_test.go hold that with no wallet and no chain, including
that a request with no action_id is refused before anything reaches Zenoh.

Through this repository's own Go tunnel, in isolation

demo_go_tunnel.py exercises the refusal paths against the tunnel alone, behind
a minimal stand-in for the relay:

proxy (stands in for the Fabric backend)
    ↓ WebSocket  ws://…/api/core/ws/robot?id=atlas-sim-01
Go tunnel  POST /action
    ↓ x402 middleware → live facilitator
Zenoh robot/tunnel/action → Atlas bridge → MuJoCo
Request The tunnel's answer Reached the simulator
No payment HTTP 402, payment requirements advertised no
Structurally valid but unsigned authorization HTTP 400, after consulting the facilitator no

The payment decision there is the tunnel's, not this profile's. Build steps are
in TUNNEL_BUILD.md.

Two payment gates, and which one proved what

  1. Protocol checks — amount, asset, network, expiry, settlement-reference
    shape, replay. Cheap, and they reject the obvious cases first.
  2. Facilitator verification — the only check that can tell a real
    authorization from a well-formed forgery, because only the facilitator
    recovers the signer.

The forgery case is proven against the live facilitator: a payload with the
right amount, asset, network and a perfectly shaped signature is refused with
invalid_exact_evm_signature. A companion test asserts the uncomfortable half —
the protocol checks alone do accept that same payload, which is precisely why
the facilitator layer exists. Verification fails closed: an unreachable
facilitator is a rejection, never an approval.

The accepting side, proven. real-paid-run.json carries one action the
whole way: an EIP-3009 authorization signed by a funded wallet, accepted by the
live facilitator (isValid: true), executed on the robot (3/3 targets,
correlated by action_id), and settled for 0.001 USDC — the price the
profile declares — in 0x2b3b71d0…c0f39, block 45706216.

The settlement is tied to the action rather than merely adjacent to it. EIP-3009
lets the signer choose the authorization nonce, so this run sets it to
keccak256(action_id); the token emits that nonce in AuthorizationUsed, so
anyone can recompute it from the action id and confirm this transfer paid for
this action:

keccak256("act-paid-de66513f791b")   = 0xaa6cf89a…fd03b47de
AuthorizationUsed nonce, block 45706216 = 0xaa6cf89a…fd03b47de

The transaction was submitted by the facilitator's own address — the one its
/supported endpoint advertises — which is independent evidence it went through
the live facilitator rather than being self-submitted, and why the payer holds
no ETH: under EIP-3009 the payer signs and the facilitator pays the gas.

Order matters and is enforced: /verify first, robot second, /settle only
because the episode reached every target. A failed episode leaves the
authorization signed and unspent.

The in-process and Zenoh demos remain protocol-level by design — they hold no
wallet — and their artifacts say so: settlement: eligible_not_on_chain,
settlement_tx_hash: null. That distinction is enforced by the ledger, not by
wording: SETTLED requires a transaction hash and its block, and
test_settled_requires_a_real_transaction fails if a receipt alone can earn it.

One actuation per payment-validated action

The idempotency store is keyed on robot_id + skill_id + idempotency_key,
records the parameters and a payment fingerprint, and persists to disk:

Repeat Outcome
Same key, same request duplicate — recorded outcome replayed, robot does not move
Same key, different parameters refused, IDEMPOTENCY_PARAMS_CONFLICT
Same key, different payment refused, IDEMPOTENCY_PAYMENT_CONFLICT
Same key after a restart still one actuation

Controller

STAND → REACH → VERIFY  ×3 targets  → RETURN → DONE
          ▲        │
          └────────┘   hold broken → re-converge

Each control tick re-reads the measured end-effector pose and the measured joint
configuration, then solves one damped-least-squares resolved-rate step on the
right-arm chain. A target only counts after the hand holds it inside tolerance
for 250 consecutive steps. No recorded trajectory is used anywhere.

Atlas is never welded, clamped or externally supported. The fall check uses real
standing height — 0.70 m against a 0.911 m stance — not floor contact.


Results

Metric MuJoCo PyBullet Webots R2025a
Status success success success
Targets reached and held 3 / 3 3 / 3 3 / 3
Mean end-effector error 9.54 mm 12.18 mm 8.98 mm
Max end-effector error 13.49 mm 19.79 mm 12.33 mm
Min pelvis height (fall below 0.70 m) 0.9084 m 0.9395 m 0.8982 m
Falls 0 0 0
Shelf collisions 0 0 0
Episode duration 4.61 s 4.98 s 7.70 s

Cross-engine agreement: mean-error spread 3.20 mm (limit 50 mm), duration
spread 3.10 s (limit 5.0 s) → sim-to-sim verdict PASS.

Repeated MuJoCo runs hash identically — the test fingerprints the whole result,
not a few metrics.


The robot

Atlas v4 is fetched, never vendored:

Upstream openai/roboschool
Commit d32bcb2b35b94168b5ce27233ca62f3c8678886f
File atlas_description/urdf/atlas_v4_with_multisense.urdf
License MIT — see NOTICE.md
Mesh assets committed none — upstream collision geometry is analytic
atlas_v4_with_multisense.urdf   ← one pinned description
        │
        ├── MuJoCo    (URDF → MJCF at load time)
        ├── PyBullet  (loaded directly)
        └── Webots    (URDF → PROTO + world at setup time)

The Jacobian and the gravity feedforward come from that URDF via
kinematics.py rather than
from each engine, so the controller is identical everywhere. Tests pin both
against MuJoCo's independently computed values (Jacobian within 5e-4, measured
worst case 2.4e-4; gravity model within 1e-3 N·m).

Actuator addressing is read out of the compiled model and validated against the
URDF's own effort limits; any drift raises immediately.


Payment

One price and one asset address, declared once and asserted against every
consumer:

Price 0.001 USDC = 1000 raw units
Asset USDC 0x036CbD53…3dCF7e
Network Base Sepolia — eip155:84532

A settlement reference that is not a 32-byte EVM transaction hash is refused with
MALFORMED_TX_HASH before anything executes.

On-chain settlement

Field Value
Action act-paid-de66513f791b
Settlement tx 0x2b3b71d0…c0f39
Status success (0x1), block 45706216
Event Transfer(address,address,uint256) on the USDC contract
Amount 0.001 USDC (1000 raw) — the price the catalogue publishes
Payer 0xa0597a74…Fc2Dc
Payee 0x7b916325…C3e8
Binding authorization nonce = keccak256(action_id)

settlement_evidence.py reads this transaction out of real-paid-run.json
rather than from a pinned hash, re-reads it from a public RPC, decodes the
Transfer log itself, and exits non-zero if it is missing, reverted, carries no USDC
transfer. The workflow runs it on every push.


Evidence

Artefact What it shows
atlas-paid-action.gif One continuous pass: 402 → signed authorization → 202 → Zenoh → execution → result → settlement, beside the episode it paid for
atlas-shelf-inspection.gif The annotated episode — same run as the metrics
mujoco-inspection-episode.json Full MuJoCo metrics, per-target accuracy
pybullet-inspection-episode.json Same task on the second engine
webots-inspection-episode.json Same task on Webots R2025a
sim2sim-validation.json Cross-engine comparison and verdict
reach-envelope.json 36-probe sweep the shelf geometry is derived from
go-tunnel-e2e-evidence.json Unpaid and forged actions refused by this repo's Go tunnel
go-tunnel-e2e-terminal.txt Terminal transcript of that run
fabric-relay-e2e.json The whole path: hosted relay, discovery, priced 402, paid action, Atlas 3/3, relay status, 0.001 USDC settled
fabric-relay-failure.json The unhappy path through the same relay: refused by the declared bounds, answered 202, reported failed with its real reason — and never charged, confirmed by the token contract
real-paid-run.json One real paid action: live facilitator accepts, robot 3/3, 0.001 USDC settled, bound to action_id
tunnel-e2e-evidence.json Payment-validated action over the real Zenoh transport
tunnel-e2e-terminal.txt Terminal transcript of that run
demo-e2e-evidence.json The in-process x402 gate and settlement ledger
onchain-settlement.json Base Sepolia settlement, re-read from a public RPC

Skill discovery and pricing are published in
skill-catalog.json,
generated from skills.yaml so the two cannot disagree.

Every committed evidence artifact is checksummed in
evidence-manifest.yaml,
and stored byte-exact so the recorded hashes still match after a clone.

Why the shelf is where it is

reach_envelope.py drives the arm to a 6 × 6 grid of offsets and records, per
probe, whether the arm converged and whether Atlas was still standing:

vert \ fwd   +0.06  +0.12  +0.18  +0.21  +0.24  +0.30
  +0.20       OK     OK     OK     OK    fall   miss
  +0.10       OK     OK     OK    miss   fall   fall
  +0.00       OK     OK     OK     OK     OK     OK
  -0.06       OK     OK     OK     OK     OK     OK
  -0.12       OK     OK     OK     OK    fall   fall
  -0.20      fall   fall   fall   fall   fall   fall

The reported envelope is the largest block where every probe succeeded
(forward 0.06–0.18 m, vertical −0.12…+0.20 m, 15/15) — not a bounding box around
scattered successes. All three targets sit inside it, and a test fails if one is
ever moved out.


Reproduce

pip install -r bridge/boston_dynamics/atlas_bridge/requirements.txt
python -m bridge.boston_dynamics.atlas_bridge.download_atlas_model
python -m pytest bridge/boston_dynamics/atlas_bridge/tests -q     # 116 passed
python -m bridge.boston_dynamics.atlas_bridge.runner              # exit 0 only on success
python -m bridge.boston_dynamics.atlas_bridge.pybullet_runner
python -m bridge.boston_dynamics.atlas_bridge.sim2sim
python -m bridge.boston_dynamics.atlas_bridge.demo_tunnel
python -m bridge.boston_dynamics.atlas_bridge.settlement_evidence

Configuration, expected success and failure payloads, safe-stop and
troubleshooting are documented in the
bridge README.


Tasks

  • Pin the Atlas v4 model with its licence and provenance
  • Remove every vendored Atlas mesh asset
  • Validate actuator mapping against the pinned URDF
  • Fail loudly on any actuator or effort-limit drift
  • Derive one shared URDF kinematics for all engines
  • Validate the Jacobian independently against MuJoCo
  • Validate the shared gravity feedforward against MuJoCo
  • Measure the free-standing reach envelope
  • Constrain the inspection targets to the validated envelope
  • Implement the closed-loop DLS inspection controller
  • Prove no recorded trajectory is used
  • Execute three sequential inspection targets
  • Require a hold before a target counts as reached
  • Detect falls from standing height, not floor contact
  • Detect shelf collisions
  • Validate MuJoCo execution
  • Validate PyBullet execution
  • Validate Webots R2025a execution
  • Complete cross-engine sim-to-sim validation
  • Wire the tunnel bridge to the registered skill and profile
  • Carry a payment-validated action over the real Zenoh transport end to end
  • Correlate every result by action_id and the tunnel's identity fields
  • Refuse unpaid, invalid, malformed-hash and replayed receipts
  • Verify payments against the live x402 facilitator, failing closed
  • Prove a forged authorization is refused before anything executes
  • Make idempotency durable — one actuation per key, across restarts
  • Accept both snake_case and camelCase tunnel envelopes
  • Gate an action through this repository's own Go tunnel and x402 middleware
  • Make the idempotency claim atomic and prove it under concurrency
  • Require the identity fields the tunnel correlates on, with no inference
  • Emit params_hash in the published sha256:<hex> form for every action
  • Record the terminal outcome so a duplicate reports how the first run ended
  • Classify each refused payment by its actual reason in the ledger
  • Add safe-stop behaviour and prove a stopped run never settles
  • Prove a failed execution never settles
  • Carry a paid action through the hosted Fabric relay end to end
  • Discover the robot, its skills and its price through the relay before paying
  • Build the payment from the discovered price rather than a constant
  • Read the terminal result back from the relay, correlated by action_id
  • Show a refused action reported as failed through the relay, with its real reason
  • Enforce the catalogue's declared parameter bounds on a paid action
  • Settle only after the robot reports success — proven on chain in both directions
  • Prove a refused action was never charged, from the token contract's own record
  • Refuse an uncorrelatable action before anything reaches Zenoh, and test it
  • Make the published functions.yaml contract match every route's real responses
  • Verify in CI that the settlement is bound to the action it paid for
  • Fail that check on a wrong amount, asset, payer or payee, not just a wrong binding
  • Make the skill contract, README, manifest and report agree on every status code and amount
  • Pin MuJoCo to the build the evidence was recorded on, after CI observed the drift
  • Build the Go tunnel in CI from a clean checkout and run its payment-contract tests there
  • Record the payment and the robot in a single continuous pass over one action
  • Refuse an action missing any identity field before it reaches Zenoh, and test each one
  • Advertise the payee the robot is paid at, and hold it to the configured one in test
  • Document wallet setup, testnet configuration and key handling in the README
  • Demonstrate a live facilitator-verified positive paid action
  • Bind the accepted EIP-3009 authorization and its settlement to the executed action_id
  • Settle the declared 0.001 USDC price on Base Sepolia, after execution only
  • Measure end-effector speed against the hand's own displacement, and report
    the speed reached while inspecting separately from the episode peak
  • Declare one price and one asset address across the whole profile
  • Verify the real Base Sepolia settlement from a public RPC
  • Publish evidence artefacts with checksums that survive a clone
  • Publish a machine-readable skill catalogue for discovery and pricing
  • Keep reproduction read-only — no command rewrites committed evidence
  • Add reproducible CI covering task, payment, tunnel and settlement
  • Add a reviewer-facing annotated GIF
  • Document the operational runbook and clean-checkout reproduction

Reviewer notes

  • Webots was validated locally, headless. It is not available on GitHub's
    runners, so CI verifies that the PROTO and the world still generate from the
    pinned URDF, and sim2sim reports the engine as
    unavailable_no_webots_installation there, scoring only the engines that ran.
    A missing engine can never turn a failing comparison into a passing one.

  • CI status. Commit 2b09fc2, the exact HEAD of this PR, passed every job on
    the author fork —
    run 32399852659,
    which includes the Zenoh tunnel walkthrough, the on-chain settlement
    re-verification, and the paid action itself on a clean Ubuntu runner. The
    upstream PR workflow shows action_required and awaits maintainer approval,
    which is the normal path for a fork; that is why this PR's own checks tab is
    empty.

  • The payment happens in the run, not beside it. The live job builds the
    tunnel from the checkout, dials the hosted Fabric relay, discovers the robot
    and the price, takes the 402, signs an EIP-3009 authorization whose nonce is
    keccak256(action_id), runs Atlas, and settles only after the correlated
    result reports success — all inside the run being reviewed. In
    32399852659 that
    was atlas-inspect-1787248406, settled in
    0x30160596…c5a32
    at block 45740066. The same job then sends an action the catalogue refuses — a
    1-second budget against a declared 5..60 — which comes back failed with
    INVALID_DURATION and settles nothing: authorizationState for
    keccak256("atlas-inspect-1787248427") is still zero. Both are reachable from
    the action id alone, without trusting the artifact. The job runs on push and
    workflow_dispatch only, because secrets do not exist in a workflow triggered
    from a fork's pull request; without a credential it still rehearses the whole
    path with --dry-run, signing nothing.

  • Where the three engines genuinely differ. Only the joint servo: MuJoCo
    integrates an explicit PD law, while PyBullet and Webots use their own implicit
    servos, all saturated at the same URDF effort limits. Webots' servo gain had to
    be raised from its default — measured, not guessed. Everything above the servo
    is shared code.

  • Every numeric claim above is generated from a committed evidence artefact.

  • Four demos, and exactly what each one runs. Only the first reaches the
    hosted relay; the others isolate a layer, and none of them stands in for a
    component the first one exercises.

    Demo Hosted relay Go tunnel Payment
    demo_fabric_e2e.py real real live facilitator, settles on success
    demo_go_tunnel.py local proxy real live facilitator, refusals only
    demo_tunnel.py not used — Zenoh directly not used protocol checks, settles nothing
    real_paid_run.py not used — Zenoh directly not used live facilitator, settles after success

Known boundaries

  • Simulator-only. No physical Atlas validation is claimed. The robot is the
    description this PR pins — atlas_v4_with_multisense.urdf from
    openai/roboschool@d32bcb2, fetched and SHA-256 verified at build time — and
    no claim is made that it is equivalent to, or interchangeable with, any other
    Atlas hardware generation.
  • Webots execution evidence is a local headless run, not GitHub-runner execution.
  • Testnet settlement on Base Sepolia; the USDC has no monetary value.
  • The paid run was executed by an operator-held wallet; no key material lives in
    this repository. Both payer wallets are disposable test wallets, treated as
    compromised, holding nothing of value.
  • The relay is the hosted service; its source is not in this repository, so
    demo_go_tunnel.py still stands in for it when exercising the tunnel's
    refusal paths in isolation. The positive path does not stand in for anything.
  • Testnet only; Base Sepolia USDC has no monetary value. Both payer wallets
    are disposable test wallets, treated as compromised, holding nothing.
  • Robot identity, the outbound client, and the payee — three things that get
    conflated, so, from the code: there is no package called robotsdk in this
    repository. The robot-side outbound client it ships is tunnel/, and the
    relay connection itself is tunnel/internal/client.go, which dials the WSS
    endpoint with gorilla/websocket. The same module also carries
    github.com/unibaseio/aip-go-sdk, used by cmd/main.go and internal/aipagent
    for the optional authenticated AIP path — not for that transport. tunnel/ is
    the component the merged Tier-1 profiles use. This profile extends its action
    contract — 202/status semantics and deferred settlement — and adds three
    read-only discovery and status endpoints; it changes no transport behaviour. The authenticating handshake lives in it: with AIP_ENABLED=true
    the tunnel runs aipauth.EnsureAuth, receives a bearer token and a wallet
    address, and registers the agent with Handle = robot id and UserID = that
    wallet. This profile does not bypass it, but the recordings here run with it
    disabled, because EnsureAuth drives an interactive browser authorization flow
    that an unattended, reproducible demo cannot perform. So: implemented in the
    component this profile uses, not exercised in these recordings, and not
    reimplemented here. The payee half is held and tested — GET /robot, the
    402 and the settlement all carry the one configured address, and the x402
    matcher refuses a payment whose payee differs. And the binding that is
    cryptographic is the settlement to the action: keccak256(action_id) as the
    authorization nonce, recorded on chain.
  • This branch predates two changes now on main.
    #59 moved the tunnel's
    x402 wiring onto ginmw.SchemeConfig with the evm/exact/server scheme and
    added registerTokenAsset for non-default payment tokens;
    #126 added MPP
    alongside x402 on the same POST /action. This branch keeps the tunnel API
    that the recorded evidence and CI were produced against, so merging main
    into it conflicts in tunnel/cmd/main.go,
    tunnel/internal/handlers/handlers.go and
    tunnel/internal/handlers/handlers_test.go — the deferred settlement gate
    sits exactly where those PRs rewrote the middleware call and the handler.
    The Zenoh envelope is not a problem: Add MPP (Machine Payments Protocol) support alongside x402 #126 keeps payment_payload and
    payment_requirements where they were and only adds protocol, and this
    profile's bridge reads those two keys and ignores what it does not know, so
    an envelope produced by the newer tunnel parses here unchanged. That is an architectural
    reconciliation, not a formatting one, and it is deliberately not attempted
    here: doing it would rebuild the component every artifact in this submission
    was recorded against. No claim is made that the two tunnel implementations
    are drop-in compatible.
    The Tier branch this PR targets sits at the commit
    the profile was built on, which is why it merges cleanly there. What has been
    checked is the conflict itself; the work to rebuild the gate on the newer
    scheme registration has not been scoped, and can follow on request.
  • Field names, mapped to the criteria. The criteria illustrate skill metadata
    as name / priceUSDC / paramsSchema, and a failure envelope as
    status: "error" carrying a nested error.code. This profile uses the
    repository's existing snake_case forms instead: skill_id, price_usdc and
    params in the catalogue, robot_id in the /skills envelope, and status
    with result.error_code in the correlated result. Every concept the criteria
    name is present and carried by the recorded artifacts and the contract tests;
    these are naming choices, not missing data. They are left alone because the
    evidence was recorded against them, and renaming the wire format afterwards
    would put the artifacts and the code out of step.
  • params_hash is derived, not carried. The Zenoh envelope preserves
    action_id, robot_id, skill_id, idempotency_key and the payment. The
    bridge computes params_hash itself, canonically, from the parameters that
    arrived, and emits it in the sha256:<hex> form execution-mapping.yaml
    declares — including for an empty parameter set. A hash supplied on the wire
    is not consulted. So the hash in every result describes what actually
    executed, and a caller cannot claim one parameter set while sending another;
    but it is a derivation rather than a comparison against a payer-supplied
    value.

@EslaM-X
EslaM-X requested a review from a team August 19, 2026 01:12
@EslaM-X
EslaM-X force-pushed the boston-dynamics-atlas-tier-1 branch 2 times, most recently from 4f6b679 to e5278f0 Compare August 19, 2026 16:33
@EslaM-X EslaM-X changed the title Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-webots-obstacle-nav.v1 Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-shelf-inspection.v1 Aug 19, 2026
@EslaM-X
EslaM-X force-pushed the boston-dynamics-atlas-tier-1 branch 2 times, most recently from 39a21af to c63126c Compare August 19, 2026 18:59
@EslaM-X EslaM-X changed the title Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-shelf-inspection.v1 Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-webots-shelf-inspection.v1 Aug 19, 2026
@EslaM-X
EslaM-X force-pushed the boston-dynamics-atlas-tier-1 branch 6 times, most recently from e2ac063 to 4a81c1b Compare August 19, 2026 21:47
…r 1)

Payment-gated, policy-driven multi-target shelf inspection on a free-standing
Boston Dynamics Atlas v4, executed over the tunnel transport and validated
across MuJoCo, PyBullet and Webots R2025a from one pinned robot description.

Payment path
  demo_go_tunnel.py drives this repository's own Go tunnel — the binary that
  mounts the upstream x402 gin middleware and a real facilitator client — behind
  a minimal stand-in for the hosted Fabric backend. The tunnel refuses an unpaid
  action with 402 and advertises payment requirements, and refuses a forged
  authorization with 400 after consulting the live facilitator; neither reaches
  Zenoh or the simulator. TUNNEL_BUILD.md records how the binary was built.
  The accepting side needs an EIP-3009 authorization signed by a funded wallet
  and is deliberately not claimed.

  demo_tunnel.py proves the Zenoh path itself with a Python client: a verified
  action is published on robot/tunnel/action, executed, and answered on
  robot/tunnel/result carrying the originating action_id, and settlement only
  follows a correlated success.

  A paid action actuates the robot once. The idempotency claim is taken
  atomically and persisted, so concurrent retries and restarts both collapse to
  a single actuation; a repeat with different parameters or a different payment
  is refused. Identity the tunnel correlates on — action_id, robot_id, skill_id,
  idempotency_key — is mandatory rather than defaulted.

  One price (0.001 USDC = 1000 raw) and one USDC address are declared in task.py
  and asserted against the registry, the demos and the settlement layer.

Robot
  Atlas v4 is fetched, never vendored: models/model.lock.json pins
  openai/roboschool@d32bcb2 (MIT). Collision geometry upstream is analytic, so
  no mesh assets are needed and none are committed. MuJoCo imports the URDF as
  MJCF, PyBullet loads it directly, and webots_env.py converts it to a PROTO.

Controller
  STAND -> REACH/VERIFY per target -> RETURN, with a damped least-squares
  resolved-rate loop re-solved from the measured end-effector pose every control
  tick. No recorded trajectory. The Jacobian and the gravity feedforward are
  derived from the URDF rather than any engine, so all three backends run the
  same maths; tests check them against MuJoCo's own.

Measured
  MuJoCo   3/3 targets,  9.54 mm mean error, min pelvis 0.9084 m, 0 collisions
  PyBullet 3/3 targets, 12.18 mm mean error, min pelvis 0.9395 m, 0 collisions
  Webots   3/3 targets,  8.98 mm mean error, min pelvis 0.8982 m, 0 collisions
  Cross-engine mean-error spread 3.20 mm; sim-to-sim verdict PASS.
  Fall threshold is 0.70 m of standing height, not floor contact.
  Repeated MuJoCo runs hash identically, and a clean clone reproduces them.

  111 tests cover the tunnel contract, the facilitator, idempotency, the payment
  gate, the model integrity, the kinematics and the task.
@EslaM-X
EslaM-X force-pushed the boston-dynamics-atlas-tier-1 branch from 4a81c1b to 1568e4b Compare August 19, 2026 22:06
EslaM-X added 15 commits August 20, 2026 01:31
The reported end-effector speed was read from `data.cvel[hand][:3]`. MuJoCo
lays `cvel` out as `[angular; linear]`, so the figure published as 5.76 m/s
was the hand's angular rate in rad/s. Measured against the hand's own
displacement it moves at 1.13 m/s, and it never exceeds 1.5 m/s.

All three engines now measure the same quantity — how far the hand travelled
in one control step — and a test checks the reported number against that
displacement, since no task assertion fails when this metric is wrong.

The episode peak turned out to be a RETURN artefact: RETURN assigns the stance
pose straight into the joint targets, bypassing the rate limit that shapes
REACH, so only the actuator limits bound the retraction. The speed reached
while actually inspecting is therefore reported separately, and the remaining
spread between engines (0.34 / 0.37 / 2.82 m/s) is servo stiffness against a
shared joint-space ceiling of 0.01 rad per 2 ms control step.

Also stops calling an action "paid" where only the payment was validated;
"paid" now appears solely where value actually settled on-chain.

Evidence regenerated on all three engines, Webots included, and every
checksum in the manifest re-verified.
demo-e2e-evidence.json recorded the accepted demo step as status SETTLED with
a tx_hash and block_number 0, and tunnel-e2e-evidence.json recorded it as
settled: true while stating payment_verification: protocol_checks_only a few
lines further down. Both demos run in-process with no wallet. A reviewer
reading the PR body would have understood the boundary; a reviewer opening
either artifact on its own would have read it as proof that USDC moved.

The fix is in the ledger rather than in the wording, so the claim cannot come
back: SETTLED now requires a settlement transaction hash and the block that
contains it. Without one the entry becomes SETTLEMENT_ELIGIBLE — the execution
succeeded and the policy authorises payment, but nothing moved — and
settlement_tx_hash is null. The receipt a caller presents and the hash of a
settlement transaction were sharing one field; they are different things, and
conflating them is what produced the misleading artifact.

The relay reports settlement_eligible instead of settled for the same reason.

Tests updated to lock the honest semantics: a receipt alone can no longer earn
SETTLED, and an eligible entry must carry no block number.
Every other demo here proves the refusing side — unpaid refused, forged
authorization refused by the live facilitator, replay refused — because that
half needs no money. This is the accepting side: a real EIP-3009 authorization
verified by the live facilitator, executed on the robot, then settled in USDC
on Base Sepolia and read back from a public RPC.

Two things make the artifact mean something rather than merely look impressive.

The authorization is bound to the action. EIP-3009 lets the signer choose the
32-byte nonce, so this sets it to keccak256(action_id). The same value appears
in the AuthorizationUsed event the token emits, which means a reviewer can
recompute it from the action id alone and confirm this settlement paid for this
action. Nothing else in an x402 receipt ties a payment to the work it bought.

Settlement happens after execution, never before: verify, run, and only then
settle, so a failed episode leaves the authorization signed but unspent.

The signing key is read from SETTLEMENT_PRIVATE_KEY or SETTLEMENT_MNEMONIC and
is never printed, logged, or written to any file. Accepting the recovery phrase
directly removes the conversion step, which is where keys tend to end up pasted
somewhere they should not be. --payer refuses to run if the configured key
signs for a different address.
Closes the last gap in the evidence: until now every payment result here was
either a refusal or a protocol-level acceptance with a synthetic receipt. This
is one action carried the whole way — an EIP-3009 authorization signed by a
funded wallet, accepted by the live x402 facilitator, executed on the robot,
and settled only after the episode reported every target reached.

  action     act-paid-de66513f791b
  verify     isValid: true, from the live facilitator
  robot      success, 3/3 targets, 0 contacts, correlated by action_id
  settle     0x2b3b71d0ce18554a4927e1145a704359bad35c209f632dc414926b995aac0f39
  chain      block 45706216, 0.001 USDC, the price the profile declares

The settlement is tied to the action rather than merely adjacent to it. EIP-3009
lets the signer choose the authorization nonce, so this run sets it to
keccak256(action_id); the token emits that nonce in AuthorizationUsed, so anyone
can recompute it from the action id and confirm this transfer paid for this
action. A receipt and an execution are otherwise two unrelated facts.

The transaction was submitted by the facilitator's own address — the one its
/supported endpoint advertises — which is independent evidence the payment went
through the live facilitator, and why the payer holds no ETH: under EIP-3009 the
payer signs and the facilitator pays the gas.

Also removes settlement.py. Its OnChainSettlement was unused and could not have
worked: the transferWithAuthorization ABI omitted v, r and s, so the call it
built had six arguments where USDC's function takes nine. Leaving code that
claims to settle but cannot is worse than not having it.
The bounty's end-to-end flow runs client -> Fabric relay -> tunnel -> Zenoh ->
simulator -> result -> relay. This tunnel could only do the middle of that: it
published paid actions onto Zenoh, but a caller had no way to ask what robot
was on the other end, what it could do, what that cost, or how a submitted
action ended. demo_go_tunnel.py papered over the gap with a local stand-in for
the hosted backend, which is exactly the substitution worth removing.

Adds three read-only endpoints and nothing else. POST /action is untouched.

  GET /robot                     what robot is connected
  GET /skills                    what it can do and what each skill costs
  GET /action/:action_id/status  how a submitted action ended

Discovery answers from the profile's own skill-catalog.json, so a quoted price
cannot drift from the price the registry publishes. Status is not synthesised:
the tunnel subscribes to the same robot/tunnel/result topic the simulator
publishes on and stores what arrives, keyed by action_id. An action nobody has
answered for reads as pending, and a failed one reads as failed — reporting
anything else would make the endpoint a decoration rather than a status.

demo_fabric_e2e.py drives the real hosted relay at api.fabric.foundation with
this tunnel dialled out to it over WSS. Confirmed working: the relay reports
the robot connected, returns both skills at the catalogue price, and refuses an
unpaid action with 402 carrying real payment requirements.

Two details found by running it rather than by reading about it. x402 v2 matches
a payment against the advertised requirements on scheme, network, amount, asset
and payTo, all read from an "accepted" object on the payload — the v1 shape of
scheme and network at the top level matches nothing and is refused with "No
matching payment requirements". And v2 names the price "amount" where v1 called
it "maxAmountRequired".

Note on ordering: the tunnel's middleware settles as part of accepting a
payment, so on this path settlement precedes execution. real_paid_run.py
settles after the episode succeeds. Both are real; they are not the same
guarantee, and the artifacts say which is which.

The handlers package test fails in this Windows environment with a DLL-loading
error, identically on the unmodified code; it is not caused by this change.
fabric-relay-e2e.json records one action carried end to end with nothing stood
in for: the hosted relay at api.fabric.foundation, this repository's Go tunnel
dialled out to it over WSS, the live facilitator, Zenoh, MuJoCo, and a real
settlement. Until now the relay was substituted by a local proxy and the paid
path reached the robot over Zenoh directly.

  discovery   GET /robots/{id}/skills -> inspect_shelf, stop @ 0.001 USDC
  unpaid      402 from the relay, carrying payment requirements
  paid        accepted
  execution   3/3 targets
  status      succeeded, correlated by action_id, read back from the relay
  settlement  0x34d34a9d931c92f32ad0e993fc5c72bf730cdadea6826ebf0de821fe41ed4cc5
              block 45707426, 0.001 USDC, nonce = keccak256(action_id)

The price is discovered rather than assumed: the payment is built from the
amount the relay quotes in its 402, and the run asserts that amount matches the
price the catalogue publishes. A profile whose advertised price had drifted from
what its tunnel charges would fail that check instead of passing quietly.

One difference between the two paid paths is recorded rather than smoothed over.
On the relay path the tunnel's middleware settles as part of accepting the
payment, so settlement precedes execution; real_paid_run.py settles only after
the episode reports every target reached. Both are real and both are bound to
their action_id, but they are not the same guarantee.
fabric-relay-failure.json sends a paid action whose maxDurationSec is below the
bound the catalogue declares. Two separate things come out of it and they are
reported separately, because merging them would flatter the result.

Failure semantics hold. The bridge refuses the action with INVALID_DURATION and
the refusal reaches the caller through the relay's own status endpoint as
state: failed, carrying the real error code rather than a generic one, still
correlated by action_id. Nothing was smoothed into a success.

Payment safety does not hold on this path. The action was refused and it was
still paid for — 0.001 USDC in
0xdb69d2d408682202fffd37a11e836cbf71c64e3869e5f9814f33498f082a0aec, block
45707813. The tunnel's x402 middleware settles as part of accepting a payment,
before the robot is reached, so a refusal downstream comes too late. That is a
property of the middleware and not of this bridge: real_paid_run.py asks the
facilitator to settle only after the episode reports every target reached. The
profile now contains both orderings and the artifacts say which is which, rather
than presenting the safer one as though it were universal. The finding is
recorded in the artifact itself under payment_safety, in the validation report,
and in the PR's known boundaries.

On why the failure is a refused parameter rather than a timeout: the catalogue
declares maxDurationSec minimum 5, and at 5 seconds the episode completes all
three targets — measured over three runs, not assumed. There is no in-bounds
duration that produces an execution timeout, which is a property of a well-chosen
bound rather than a gap.

Also fixes the invariant that reported this run as failing. It compared
targets_completed against targets_total, which are both absent when an action is
refused before execution, so equal-and-missing read as "every target reached".
The checks now assert what actually matters: the state is failed, the reason is
the real one, and no successful execution is claimed.
Section 9.1 reported the status timestamp as 23:58:30Z. The artifact says
23:58:31Z. One second, and nothing depends on it — but it was substituted rather
than read, which is the one thing an evidence document cannot afford. Now taken
from the artifact, in full ISO form, alongside the action_id so the report, the
manifest and the PR body all name the same run.

The section also read as a single result. It is two, and only one of them
passes: execution failure semantics pass — the refusal reaches the caller as
failed, with its real reason, correlated by action_id — while post-execution
settlement semantics fail on the hosted relay path, because the middleware
settles before the outcome is known. Stated that way rather than averaged into
one verdict, and no compliance with post-execution settlement is claimed for
that path.
The previous revision settled on acceptance: the x402 middleware charged the
payer as soon as the payment was verified, before the robot had run, so an
action the bridge then refused was still paid for. That was measured rather
than suspected, and it is the one thing the payment policy must not do.

The middleware already had the hook. It settles after the route handler returns
and skips settlement when the response is an error, which means the status code
POST /action chooses *is* the settlement decision. So the handler now waits for
the robot's own answer over Zenoh before replying:

  200  every target reached          -> settle
  502  the robot reported a failure  -> do not settle
  504  the robot never answered      -> do not settle
  400  no action_id to correlate on  -> do not settle

An outcome that cannot be correlated cannot be known, so it cannot be paid for
either. No middleware change and no new endpoint; POST /action keeps its route
and its request shape.

Proven twice over. Six tests in handlers_test.go hold the contract with no
wallet and no chain, by injecting the transport. And through the hosted relay
with a real wallet:

  succeeded  atlas-inspect-1787193393
             0x2eca1865602dc880224ab762be20f93ba0b0c81e4bd26654445bca9d3868cd63
             block 45712565, 0.001 USDC, authorizationState = true
  refused    atlas-inspect-1787193428
             HTTP 502, no transaction, authorizationState = false

That last field is the point. "We recorded no transaction hash" is an absence of
evidence and proves nothing about whether the payer was charged. EIP-3009 tokens
keep their own map of spent authorization nonces, and because the nonce is
keccak256(action_id) a reviewer can recompute it from the action id and ask USDC
directly whether that action was ever paid for.

One subtlety worth recording: the query is pinned to the block containing the
settlement and asked only after the chain head passes it. A public endpoint
serves a receipt before it has applied the block's state, and answering from
that window reports a spent authorization as unspent — wrong in the direction
that would flatter this profile.

Sections 9 and 9.1 of the validation report and the manifest's settlement
summary are rewritten around the corrected behaviour; the stale claim in
section 7 that the accepting side of facilitator verification is not proven is
gone, replaced by a table saying which artifact proves what.
The entry for fabric-relay-failure.json said the refused action was "settled
anyway, because this path settles on acceptance". That stopped being true in
the previous commit, and a manifest is exactly the file a reviewer trusts to
describe what an artifact contains.
Four corrections, none of them to the robot.

**A refused action reached the robot.** PostAction registered a waiter, published
to Zenoh, and only then noticed the request carried no action_id — so a request
that was about to be refused had already gone out on the wire. The validation now
happens before anything is published. The test that was supposed to cover this
only asserted the status code was >= 400, which passes just as happily when the
action has already been sent; it now asserts nothing was published, and a table
test covers the other refusal paths. Reintroducing the old ordering makes both
fail, which is the check that they are worth having.

**The published contract disagreed with the code.** functions.yaml promised 202
accepted/pending; the route answers 200, 502, 504 or 400. The contract now says
what runs, including why it is synchronous: the x402 middleware settles as soon
as this route answers without an error, so a 202 charges the payer before the
robot has run and a failed episode is still paid for. That was measured on this
branch, not assumed. Deferring settlement past the middleware is not available
to a route behind it, so the route waits instead — and the asynchronous read is
still there as GET /action/{action_id}/status, which reports pending until the
result arrives. The declared responses for /robot, /skills and the status route
now match their handlers too.

**A clean checkout could not run the paid path.** real_paid_run.py and
demo_fabric_e2e.py import eth_account and eth_utils, which were in no
requirements file. Added, with a note on what they are for.

**CI re-verified the wrong transaction.** The step called "Re-verify the on-chain
settlement" checked a 1.0 USDC transfer that predated the paid run and was bound
to no action, while the profile's real settlement was 0.001 USDC. It now reads
the settlement out of real-paid-run.json and fails unless the transaction
succeeded *and* its AuthorizationUsed nonce equals keccak256(action_id) — a
transfer of the right size that is not bound to the action proves the asset
moved, not that this action was the reason.

Also fixes a lifecycle bug: the result subscription is declared once per process
while setupRouter builds a fresh Handlers on every config restart, so the new
handler had been reading an empty store and would have reported every action
pending for ever. The store is now shared with the subscription; tests override
it for isolation.
The success criteria ask for two things that the stock x402 middleware cannot
both give: an immediate accepted/pending response, and no settlement when the
action fails. That middleware settles as soon as a protected route answers
anything under 400, so a 202 charges the payer before the robot has run. The
previous commit chose payment safety and answered 200/502/504 after waiting,
which met the second requirement by breaking the first — and left the published
contract describing a 202 the code never sent.

Both now hold. A gate replaces the stock middleware and keeps its 402/verify
half unchanged: an unpaid request still gets 402 with the advertised
requirements, and a payment the live facilitator rejects still never reaches the
robot. Instead of settling, it hands a settlement callback to the handler.
POST /action publishes, answers 202 with the action_id and a status URL, and a
background watcher invokes that callback only when the correlated result reports
success. A failure or a silent robot leaves the authorization signed and unspent.
Both halves are readable from GET /action/{action_id}/status, which now carries
the settlement record alongside the execution state.

Verified end to end through the hosted relay, on chain both ways:

  succeeded  atlas-inspect-1787197727 — 202 immediately, then
             0xfd9eda75ddc6c6f979eb2571e6e85ef3a6f50d670f3f8ad252107723e21e6940
             block 45714728, 0.001 USDC, authorizationState = true
  refused    atlas-inspect-1787197752 — 202 immediately, no transaction,
             authorizationState = false

Eight tests hold the contract without a wallet: 202 carries the action_id,
success settles exactly once, failure and timeout settle zero times, and a
request with no action_id is refused before anything reaches Zenoh.

Also corrects three things the contract claimed and the code did not.
functions.yaml never declared skill_id, which the bridge requires and refuses to
infer. skill-contract.test.yaml still asserted a 1.0 USDC settlement — the price
has been 0.001 everywhere else for some time — and described the Zenoh
walkthrough as settled when that demo holds no wallet and settles nothing.
Four surfaces still described earlier revisions of this profile, and a reviewer
reading any one of them would have found it contradicting another.

skill-contract.test.yaml asserted that a valid request answers 200 and settles.
Both halves were stale in different ways: 200 is right for the in-process relay,
which is what that case actually covers, but that relay holds no wallet and
settles nothing; and the hosted path answers 202. The case now says which path
it describes and reports settlement as eligible, and three cases are added for
the hosted contract — accepted immediately, settles only after success, and
never settles a failure — each pointing at the Go test that holds it.

The bridge README's payment table read as though the in-process relay settled,
and cited a 1.0 USDC transfer as this skill's settlement. It now separates the
in-process table from the two paid paths and cites the 0.001 USDC settlement
bound to its action_id.

evidence-manifest.yaml said the tunnel answered 502 for the refused action, two
lines above recording 202. The 502 was true of the previous revision. It also
carried the unrelated 1.0 USDC transfer, which invited the question of whether
the price is 0.001 or 1.0; it is 0.001 everywhere, and a transfer bound to no
action is not evidence for this skill, so it is gone from the reviewer-facing
manifest and from section 8.2.

settlement_evidence.py verified the binding but only reported the rest. It now
fails on a mismatched amount, asset, payer or payee as well, with the expected
values read from the profile and the paid run rather than restated — so a price
changed in one place cannot leave this check agreeing with a stale copy of
itself. Confirmed by asking it to expect a different price and watching it
refuse the real transaction.
It said a payment the facilitator will accept 'is not claimed here'. True of
that demo, which has no wallet and stands in for the relay with a local proxy —
but read as a statement about the profile it contradicts two artifacts that
settle real USDC. It now says which demo proves what.
Two gaps, both about what a reviewer can check rather than take on trust.

The workflow exercised the Python bridge, the Zenoh walkthrough, the live
facilitator and the settlement, but never built the Go tunnel — so "the tunnel
in this PR builds from a clean checkout" was a claim with nothing behind it. A
job now fetches zenoh-c, builds the binary, vets it, and runs the handler tests
that hold the payment contract: a 202 says nothing about the outcome, success
settles exactly once, failure and timeout settle zero times, and a request that
cannot be correlated never reaches Zenoh. All without a wallet or a chain.

The second is a boundary rather than a fix. The payee is configured and the
settlements did go to it, but that is configuration matching, not a
cryptographic binding between the robot's identity and its wallet — that
handshake lives in the shared upstream tunnel and gateway, and this profile
consumes the identity it is given. Section 9.2 says so plainly, and distinguishes
it from the binding this profile does prove: the settlement to the action, via
keccak256(action_id) recorded in the token's AuthorizationUsed event.
The profile has a GIF of the robot and JSON artifacts for the payment, and a
reviewer has to hold the two side by side and take on trust that they describe
the same run. This records them together: a terminal pane that fills in as the
real HTTP exchange happens — discovery, the 402 with its quoted amount, the
signed authorization, the 202, the correlated result, the settlement and the
BaseScan link — beside the simulator rendering the episode that exchange paid
for.

It is one pass over one action, not two runs edited together. The bridge is
given a rendering executor, so the frames are of the episode the paid action
triggered; rendering a second, unpaid episode afterwards would look identical
and prove nothing about the first. AtlasZenohBridge takes that executor as a
parameter, which is the only change outside the new module.

--dry-run records discovery and the 402 only, signs nothing and spends nothing,
which is enough to check the layout.
atlas-paid-action.gif is a single pass over one action: the terminal pane fills
in with the real exchange — discovery, the 402 and its quoted amount, the signed
authorization, the 202, the correlated result, the settlement and the BaseScan
link — beside the simulator rendering the episode that payment bought. The
existing atlas-shelf-inspection.gif stays; it is the close-up of the task, and
this one is the whole flow.

  action     atlas-inspect-1787200740
  settlement 0xea4353bf089b08c1758bba6a27e4faa45503c0536a2a8b921d9e7de94486abb8
             block 45716247, 0.001 USDC
  nonce      0x24bfe0a79140d5b24e4b8df20a648e3c1bba3ad5db42e30b5ae6b2de50a44ae5

That transaction was located on chain by its nonce rather than by its hash:
keccak256(action_id) is enough to find the AuthorizationUsed event, which is the
binding working in the direction a reviewer would actually use it — start from
the action, arrive at the payment.

Also widens the log pane's line budget. At the previous spacing the relay URL
had scrolled off by the time the settlement arrived, which is the one moment a
reviewer is most likely to pause on.
…inding

The tunnel checked only action_id before publishing. The simulator bridge
requires four identity fields and refuses an envelope without them, so a request
missing robot_id, skill_id or idempotency_key was put on Zenoh and rejected at
the far end — an invalid request reaching the transport at all. All four are now
checked before anything is published, and the table test covers each one
missing, asserting the publisher stayed untouched.

On identity and payee binding, section 9.2 now says which half this profile can
speak for. The identity the tunnel answers for and the address it is paid to come
from one configuration and are advertised together, the 402 quotes that same
payee, the x402 matcher refuses a payment whose payTo differs, and the
settlements landed there — two tests hold the advertising half. The
authenticating handshake that would make the identity unforgeable rather than
declared belongs to the shared tunnel and gateway; this profile consumes it and
does not claim it. A robot profile is not the layer that can.

The README gains what the criteria ask of it and did not have: wallet setup
(including that no ETH is needed, because under EIP-3009 the payer only signs),
the testnet configuration in one table, and both key variables. Its Zenoh line
also said a reachable router was required while the walkthrough two pages up ran
in peer mode without one.
…tion

Section 9.2 said the authenticating handshake was upstream and unproven, and
left it there. Reading the tunnel rather than reasoning about it gives a more
exact answer, and a better one.

There is no package called robotsdk in this repository. The robot-side outbound
client it ships is tunnel/, which depends on github.com/unibaseio/aip-go-sdk and
dials the relay over WSS — the same component the merged Tier-1 profiles use,
and the one this profile uses unmodified apart from three read-only endpoints.

The handshake is in it. With AIP_ENABLED=true, cmd/main.go runs
aipauth.EnsureAuth, which returns a bearer token and a wallet address, and
internal/aipagent registers the agent with Handle set to the robot id and UserID
set to that wallet. That is identity bound to wallet, performed by the shared
tunnel through the SDK. This profile does not bypass it; the recordings run with
it disabled because EnsureAuth drives an interactive browser authorization flow
that an unattended, reproducible demo cannot perform.

So the honest split is: implemented in the component this profile uses, not
exercised in these recordings, not reimplemented here — and separately, the
payee half, which this profile does hold and test, is unchanged.
@EslaM-X

EslaM-X commented Aug 20, 2026

Copy link
Copy Markdown
Author

Hi @Junzhe 👋

Atlas Tier 1 is ready for review. Implementation, simulator evidence, sim-to-sim validation, the paid action flow, the Go tunnel, CI and the reproducibility docs are all complete, and I'd really appreciate a look when you have a chance. Happy to address any feedback.

One clarification I'd rather ask than assume — the robotsdk requirement

The Tier 1 criteria say the bridge connects outbound to the Fabric relay using robotsdk. This repository doesn't contain a package by that name. Being precise about what is actually there:

  • the outbound relay connection is tunnel/internal/client.go, which dials the WSS endpoint with gorilla/websocket directly and identifies itself with ?id=<robot_id>;
  • github.com/unibaseio/aip-go-sdk is in the same module but is used by cmd/main.go and internal/aipagent for the optional authenticated AIP path, not for that WSS transport.

Could you confirm whether this repository's tunnel/ is the intended robot-side implementation of that requirement, or whether a separate robotsdk client is expected for Atlas Tier 1? I'd rather confirm it with you than claim compliance on an interpretation.

Separately, on identity: the tunnel does contain the authenticated registration path (AIP_ENABLEDaipauth.EnsureAuth → robot Handle + authenticated wallet UserID). It is implemented but not exercised in the recorded demos, because it needs interactive browser authorization that an unattended, reproducible run can't perform. §9.2 of the validation report states that rather than claiming the handshake was demonstrated.

Thanks for taking the time 🙏


Edited to correct one detail: an earlier version of this comment said the outbound client depends on aip-go-sdk. The module does; that client does not — it uses gorilla/websocket. The distinction matters to the question, so I fixed it rather than leave it standing.

None of these change behaviour; all four described a revision this profile no
longer has, and a reviewer opening any one of them would have found it
contradicting the code.

The profile README still showed the paid case as `200 … settled`. That was the
synchronous revision. The route answers `202`, the outcome arrives on the status
endpoint, and settlement follows the result — the table now says that, including
that a replay surfaces in the correlated result rather than as an HTTP code,
because the tunnel has already answered by then.

functions.yaml declared 403, 409, 429 and 503 for POST /action. The route
produces none of them: it answers 202, 400, 500 or 502, and 402 comes from the
payment gate. Declaring codes nothing emits invites a reviewer to look for
behaviour that is not there, so the list is now exactly what the handler can
return, with a note on where the outcomes the robot decides actually appear.

skill-contract.test.yaml listed three mandatory identity fields where the bridge
and the tunnel require four — robot_id was missing. Added, along with a case for
the tunnel-side refusal that keeps an incomplete identity off Zenoh entirely.

And a comment in demo_fabric_e2e.py still said POST /action blocks until the
robot answers. It has not since the 202 contract landed; the timeout it explains
is for the status polling that follows.
The contract's header claimed every expectation below it is enforced by an
executable test under bridge/boston_dynamics/atlas_bridge/tests. Most are — 27 of
36 — but four are held by Go tests under tunnel/internal/handlers and five by
demos and verification scripts that exit non-zero when their invariants fail. The
header now says that, and points at the enforcedBy field on each case, so a
reviewer checking one is not surprised to find it somewhere other than where the
header promised.
sign_for() fell back to the compiled-in price, payee, asset and USDC metadata
whenever the 402 omitted them. Nothing exercised that path — the relay quotes
all of it — but the run exists to show the payment is built from what the robot
asked for, and a fallback means it could have passed on a number the relay never
sent. Every field is now taken from the 402 and a missing one is fatal, which is
checked by dropping each field in turn and watching it refuse.

Three contract corrections alongside it, all cases of a document describing a
route that answers differently.

skill-contract.test.yaml expected 409 for a replayed receipt. That is the
in-process relay's answer and it is correct there, but the case did not say so,
and on the hosted path the tunnel has answered 202 long before the bridge
recognises a duplicate. The case now names its path, and a second case covers
the hosted one: 202, no second actuation, and the refusal arriving as the state
and error_code of the status endpoint.

functions.yaml declared only 200 for GET /skills, while the handler answers 503
when the catalogue is unreadable — it says so rather than serving an empty
catalogue, and now the contract says so too.

A comment in demo_fabric_e2e.py still described the middleware settling on
acceptance. It has not since the deferred gate landed; the header it explains is
now only useful on the rejecting path.
The contract described 402 as covering refused payments and left 400 to body
problems, which does not match what the gate does. Reading it rather than
inferring: `extractPaymentV2` failing returns 400 with "Invalid payment", and
every other refusal — no payment, no matching requirement, a facilitator that
says no — comes back as 402 with the requirements attached. The line between
them is whether the payment could be read at all, not how badly it failed.

The contract now says that, and it matches the recorded evidence: the unpaid
action in go-tunnel-e2e-evidence.json is 402, the forged one is 400.

Worth noting because the obvious tidy-up would have been wrong. Narrowing 402 to
"payment missing" and moving refusals to 400 reads sensibly and contradicts both
the library and a live run here, where an unfunded but well-formed authorization
came back 402 with invalid_exact_evm_insufficient_balance.
The 400/402 descriptions quoted an error string from inside the x402 library. It
was accurate, but it is an upstream implementation detail that a dependency bump
could change and that a reviewer cannot check from this repository. Both are now
described by observable behaviour and each cites the artifact here that shows
it: the unpaid action in go-tunnel-e2e-evidence.json is 402, the forged one is
400. Neither claim now depends on a string this repo does not own.

A sweep for a 409 without a named path found one the earlier passes had missed.
Section 6's table is the in-process relay's and its 409 is right there, but the
row did not say so — and by section 9 a reader has three different replay
answers in mind. The row is now followed by all three: 400 on the transport
demo, 409 in-process, and 202 then DUPLICATE_ACTION on the hosted path, with
none of them actuating the robot twice.

The same sweep caught the validation report still saying the outbound client
"depends on aip-go-sdk and dials the relay over WSS" — the imprecision already
corrected in the maintainer comment and the PR body, left standing in the third
place it appears. The report now separates gorilla/websocket for the transport
from aip-go-sdk for the optional AIP path, so all three agree.
…ded on

CI went red on a commit whose only changes were to two documentation files. It
had installed MuJoCo 3.12, released since the previous run, and 3.12 settles the
resting arm 2.3 cm higher than 3.11 — the hand rests at 0.9816 m where the
recorded geometry says 0.9589 m — so
test_home_pose_matches_the_recorded_geometry failed.

The episode barely noticed: mean position error moved by 0.00001 m, the pelvis
floor not at all, and the task still reported 3/3 on both engines. But the shelf
coordinates in task.py were chosen from a reach envelope measured at that
resting pose, so the committed geometry and the engine that produced it have to
travel together. requirements.txt floored mujoco, numpy and pybullet without
bounding them, which meant a reviewer cloning the repository would get today's
releases rather than the ones docs/evidence was recorded on. They are bounded
now.

The tempting fix was to widen the test's tolerance. That would have turned a
working reproducibility check into a decoration — and the check is what found
this, on a documentation-only commit, which is exactly when nobody would have
been looking.
Two corrections to the previous commit, both about claiming more than was seen.

It bounded numpy and pybullet alongside mujoco. Only mujoco's drift was ever
observed; the other two were bounded on suspicion, which costs anyone building
this and buys nothing demonstrable. They are floored again. The integrity tests
are the net that caught mujoco on a documentation-only commit, and they would
catch these the same way.

And it said MuJoCo 3.12 settles the arm higher than 3.11, which is a claim about
what changed inside the engine and was not established. What was established is
narrower and enough: a clean CI environment resolved 3.12.0 and measured the
resting hand at 0.9816 m against the 0.9589 m recorded with the evidence build.
The report now says that and states explicitly that no cause is being claimed.
The recorder drove its own simulation loop for the frames and then called
run_episode again, on a fresh environment, for the metrics and the result that
went back to the bridge. So the GIF showed one episode and the numbers beside it
came from another, while the PR claimed the two describe the same paid action.
Determinism made them agree; agreeing is not being the same run, and the claim
was for the second thing.

run_episode's on_step now carries the controller's plan, which is what the
recorder needed the second loop for, and the recorder renders from inside the
shared scored loop. One environment, one controller, one episode: 2303 control
steps produce 57 frames at a stride of 40, and the metrics are the committed
ones. stop_requested reaches that episode too, so a safe stop now halts the run
being recorded rather than a different one.

The PR body said POST /action is unchanged and that the tunnel is used
unmodified apart from three read-only endpoints. Neither is true: the route
refuses an incomplete identity before publishing, answers 202 with an action_id
and a status URL where it used to answer 200, and defers settlement to a
watcher, with the stock middleware replaced by a gate to allow it. That is a
contract change and the diff shows it, so the description says it.

Also: mujoco is pinned to 3.11.0 exactly rather than <3.12, because a 3.11.x
patch is a different build from the one the numbers were recorded on. A failed
result subscription no longer burns a sync.Once and leaves the status endpoint
answering pending for the life of the process. And a waiter registered before a
publish that then fails is released instead of waiting for an answer that is
never coming — with a test that fails if the release is removed.
The committed recording was produced by the version that rendered one episode
and scored another. The code was fixed in the previous commit; this replaces the
artifact it made, so the file and the claim about it now agree.

  action     atlas-inspect-1787242629
  settlement 0x7fc1c1ff21e535376bf22b3409b751d769dadfdcb2491d3803c5712905b5b197
             block 45737187, 0.001 USDC
  nonce      0x97b7245b38f9b8d38bd14485f3a912f57621039933d21480dae6fb7a44aa0c40

Located on chain by that nonce rather than by the hash: keccak256 of the action
id is enough to find the AuthorizationUsed event, which is the binding used in
the direction a reviewer would use it — start from the action, arrive at the
payment.

The frames now come from inside the scored run, so the numbers beside the robot
are that robot's, not a second episode's that happened to agree.
@EslaM-X

EslaM-X commented Aug 20, 2026

Copy link
Copy Markdown
Author

@Junzhe — this Atlas Tier 1 submission is out of draft and ready for review at 7ebefaf.

What passes at that exact HEAD. Five jobs, green on the author fork — run 32391512680: unit and contract tests; the inspection episode scored on MuJoCo, PyBullet and Webots R2025a; the Go tunnel built from a clean checkout with its payment-contract tests; the x402 payment gate; and a secret scan over the diff. Two of those steps are live rather than replayed — the x402 facilitator is called and must refuse a forged authorization, and the settlement is re-read from a public Base Sepolia RPC and checked against what this profile declares.

What CI does not do, stated plainly. The paid request itself is not executed inside a workflow run. It was run against the hosted Fabric relay from an operator machine, since this fork holds no payment secrets. That settlement is verifiable without trusting the artifact: the EIP-3009 authorization nonce is keccak256(action_id), so AuthorizationUsed on USDC locates the transaction from the action id alone — 0x7fc1c1ff…b5b197, block 45737187, 0.001 USDC. The recording at the top of the description is rendered from inside that same scored episode, not from a second run beside it.

If an exact-HEAD run in a trusted context that performs the paid request itself would help your verification, I can add a push-gated job for it.

This PR's own checks tab is empty because workflows on fork pull requests wait for maintainer approval.

The boundaries that are not demonstrated — the robotsdk naming question, the AIP handshake being implemented but not exercised, and this branch predating the custom-token work now on main — are written out at the end of the description rather than left implicit. Happy to supply any further evidence you want to see.

The settlement this profile shows was real, verified on chain, and produced on
an operator machine. That last part is the weakness: a reviewer had to trust an
artifact rather than watch the payment happen at the commit being reviewed.

This adds a job that performs it in the run itself — the tunnel built from this
checkout dials the hosted relay, the robot is discovered, the price is read from
the catalogue, the relay answers 402, an EIP-3009 authorization is signed with
keccak256(action_id) as its nonce, Atlas executes, and settlement follows the
correlated result. The refused action runs in the same job and settles nothing.

It is gated on push and workflow_dispatch because secrets do not exist in a
workflow triggered from a fork's pull request, and it opts out of
cancel-in-progress so a settlement in flight is never cut in half. Without the
credential it still rehearses the entire path with --dry-run, signing nothing,
so a broken harness surfaces before money does rather than after.

The key is read from the environment by code that already refuses to print,
log, or write it. Nothing in this commit contains one.
The signer reads a private key or a mnemonic and refuses to print either. The
workflow wired only the first, so a repository holding the second would have
skipped the paid steps while still reporting green — the worst kind of pass.
Both are wired, and the gate treats either as sufficient.

The three commands are also written out again: a heredoc had collapsed their
continuations into one long line. Same command, legible to whoever reads the
workflow to check what the paid step actually runs.
Two defects in the job added two commits ago, both found by reading the evidence
it produced rather than its colour.

`python … | tee file` exits with tee's status, which is always zero. The refused
run recorded two broken invariants — "the status endpoint reported the action
failed" was false, because the action had succeeded — and the step reported
success anyway. A job that cannot go red is not a check. All three steps now set
pipefail.

The refused step also never asked for a refusable action. --expect-failure
asserts that the episode failed; it does not cause it. With the default 30s
budget the inspection completed normally, so the assertion was bound to break.
It now sends 1s, outside the 5..60 the catalogue declares, which the bridge
refuses before the robot moves.

Nothing settled for that action either way: authorizationState for
keccak256("atlas-inspect-1787248056") is still zero on chain.
The previous commit message said all three steps set pipefail. Two did. The
rehearsal was left as it was because the edit that was supposed to change it
silently matched nothing, and the claim went in unchecked — the same shape of
mistake the commit was fixing. It is set now, and the command is written across
lines again rather than collapsed into one.
@EslaM-X

EslaM-X commented Aug 20, 2026

Copy link
Copy Markdown
Author

Following up on my previous comment, which said the paid request is not executed inside a workflow run. That is no longer true, and the correction is the point of this note.

The live job now performs it in the run itself: the tunnel is built from the checkout, dials the hosted Fabric relay, discovers the robot and reads the price from the catalogue, takes the 402, signs an EIP-3009 authorization whose nonce is keccak256(action_id), runs Atlas, and settles only after the correlated result reports success.

In run 32399852659 at 2b09fc2, the exact HEAD of this PR:

  • paidatlas-inspect-1787248406, settled in 0x30160596…c5a32, block 45740066, 0.001 USDC
  • refusedatlas-inspect-1787248427, a 1-second budget against the declared 5..60, returned failed with INVALID_DURATION, settled nothing

Neither needs the artifact to be believed: keccak256 of each action id gives the authorization nonce, and authorizationState on USDC answers 1 for the first and 0 for the second.

Two things worth stating rather than leaving to be noticed. The job is gated on push and workflow_dispatch, because secrets do not exist in a workflow triggered from a fork's pull request; with no credential present it still rehearses the whole path with --dry-run and signs nothing. And the first version of this job was wrong in a way that mattered — python … | tee returns tee's exit status, so a run whose invariants failed was reported green. That is fixed with pipefail, and the commit that fixed it is in the history rather than squashed away.

Nothing else changed: the recording, manifest, checksums and settlement evidence are as they were.

@EslaM-X

EslaM-X commented Aug 20, 2026

Copy link
Copy Markdown
Author

@shicaih @fabricfoundationdev
@Junzhe — this one is ready for review at 2b09fc2, and everything below can be checked without taking the artifacts on trust.

Add robot profile: boston-dynamics/atlas/boston-dynamics.atlas.mujoco-pybullet-webots-shelf-inspection.v1 — Boston Dynamics Atlas v4, simulator-only, MuJoCo + PyBullet + Webots R2025a from one pinned URDF.

All six checks pass on the exact HEAD in run 32399852659, and the paid request is performed inside that run rather than alongside it.

Every hash in that run's evidence is one of three kinds, and each recomputes:

value how to reproduce it
settlement 0x30160596…c5a32 receipt on Base Sepolia, block 45740066, 0.001 USDC
authorization nonce keccak256("atlas-inspect-1787248406")authorizationState on USDC answers 1
refused action's nonce keccak256("atlas-inspect-1787248427")authorizationState answers 0
params_hash sha256 of the canonical parameters — {"maxDurationSec":20} for the paid action, {"maxDurationSec":1} for the refused one

The refused action asks for a 1-second budget against the 5..60 the catalogue declares, so the bridge answers failed with INVALID_DURATION and nothing settles. Both outcomes are reachable starting from the action id alone.

The boundaries this does not demonstrate are written out at the end of the description rather than left implicit: the robotsdk naming question, the AIP handshake being implemented but not exercised in an unattended run, and this branch predating the custom-token work now on main.

Happy to add anything further that would help you verify it.

@EslaM-X

EslaM-X commented Aug 20, 2026

Copy link
Copy Markdown
Author

@Junzhe @fabricfoundationdev

Submission-window clarification

Hi maintainers — Atlas Tier 1 is ready for review and has been publicly submitted before the Aug 21 date referenced for the bounty.

Could you please confirm whether the bounty window requires the PR to be opened/submitted before the deadline, or whether it must be merged before then?

PR #120 currently has the complete implementation, committed evidence, passing CI, simulator validation, and verified Base Sepolia settlement evidence.

If there is any remaining blocker for Atlas Tier 1, please point it out and I'll address it immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant