# zkloop PoB Agent Skill

Date: 2026-04-03

This document is for AI agents that can execute JavaScript or Node code.

`pob-w3` is not a simple REST integration. The real execution surface is the browser-oriented JavaScript runtime in `adapter.js` and `pob_1193.js`, which can also run headlessly under Node using the bootstrap in `node_runtime.mjs`.

## What This Skill Is For

Use this skill when the user gives an intent such as:

- "set up my PoB account"
- "charge $100"
- "enroll fuel merchant"
- "unlock fuel pump #10"
- "top up my balance"
- "settle the fueling session"
- "enroll another service"

Do not look for a single ready-made API for each intent. Instead, map the intent to a plan over the existing JS primitives.

## Execution Model

The adapter is stateful.

Important state:

- authenticated user session
- wallet/private key loaded from the encrypted wallet store
- connected RPC provider
- selected contract stored in `window.__selectedContract`
- selected contract name stored in `window.__selectedContractName`
- API owner tokens stored in local storage under `pob_api_tokens_by_owner_v1`
- customer-to-merchant settlement wallet mappings stored in the wallet

Because of that, many flows require multiple calls in the right order and sometimes multiple simultaneous sessions.

## Canonical Runtime

In a browser:

- load `window.SPAAdapter`

In Node:

1. import and run `setupNodeRuntime()` from `node_runtime.mjs`
2. import `createSPAAdapter()` from `adapter.js`
3. use one adapter instance per active identity or role

Example bootstrap:

```js
import { setupNodeRuntime } from "./node_runtime.mjs";

await setupNodeRuntime();

const { createSPAAdapter } = await import("./adapter.js");

const customerApi = createSPAAdapter({});
const settlementApi = createSPAAdapter({});
```

## Core Primitives

Auth and user lifecycle:

- `register({ userId, password, rpcUrl?, walletType? })`
- `login({ userId, password, rpcUrl? })`
- `logout()`

Funding:

- `saveBank(userId, bank)`
- `cashIn(amount)`
- `cashOut(amount)`
- `tokenBalance()`

Contract and merchant discovery:

- `connectWallet(rpcUrl)`
- `getAllContractNamesFromRegistry()`
- `getContractFromRegistry(contractName)`
- `getMerchantConfiguration(contractAddress)`
- `getSettlementWalletsForContract(contractAddress)`
- `getStoredApiTokensForContract(contractAddress)`

Merchant payment flow:

- `generateMerchantApiToken()`
- `merchantRegister(merchantContractAddress, merchantOwnerUserId, merchantOwnerPassword)`
- `merchantHold(amount)`
- `merchantSettle(txId, finalAmount)`

## Contract Selection

Some payment functions depend on the currently selected contract rather than receiving the contract as an explicit argument.

Before calling merchant payment functions, set:

```js
globalThis.__selectedContract = contract;
globalThis.__selectedContractName = contractName;
```

where `contract` is the result of `getContractFromRegistry(contractName)`.

If the selected contract is missing, `merchantHold()` and `merchantSettle()` will fail.

## Session Patterns

### 1. Customer session

Use for:

- registration
- login
- wallet connection
- bank linking
- cash-in / cash-out
- merchant enrollment
- opening escrow holds

### 2. Merchant settlement session

Use for:

- settlement wallet login
- settling a previously opened hold

The settlement user ID is created during merchant enrollment and typically has the form:

```text
<merchantOwnerUserId>/<customerUserId>
```

Do not assume the same session can both open the customer hold and settle it. The settlement path requires the active account to match the merchant settlement wallet.

## API Tokens

Merchant flows depend on long-term API tokens for:

- merchant owner
- fee owner / operator

These tokens are stored by owner address in local storage. Agents may either:

- generate them by logging in as the correct owner and calling `generateMerchantApiToken()`
- or preload them into local storage when operating in a trusted automation environment

Example preload:

```js
localStorage.setItem(
  "pob_api_tokens_by_owner_v1",
  JSON.stringify({
    [merchantOwnerAddress.toLowerCase()]: merchantOwnerToken,
    [feeOwnerAddress.toLowerCase()]: feeOwnerToken,
  }),
);
```

## Enrollment Is Not One-Shot

The user lifecycle is long-lived. Expect repeated operations such as:

- funding the same wallet again later
- enrolling the same user in another merchant/service
- reusing existing enrollment for future holds
- checking balance before service use
- topping up only when balance is too low

Do not force everything into a single monolithic procedure. Instead, inspect the current state first and perform only the missing steps.

## General Planning Strategy

When the user expresses an intent, do this:

1. identify the active user and target service
2. restore or create the right session
3. inspect current state before writing:
   - is the user already registered?
   - is the wallet already connected?
   - is the service already enrolled?
   - is the balance sufficient?
   - are owner tokens available?
4. perform only the required write actions
5. verify the resulting state
6. return the meaningful artifacts:
   - wallet address
   - settlement user ID
   - hold `txId`
   - transaction hash
   - resulting balance or readiness state

## Common Intent Decompositions

### "Set up my account"

Possible decomposition:

1. `register(...)` if needed
2. `login(...)`
3. `connectWallet(rpcUrl)`
4. report address and current balance

### "Charge $100"

Possible decomposition:

1. `login(...)`
2. `connectWallet(rpcUrl)`
3. inspect or link a bank account
4. `cashIn("100.00")`
5. `tokenBalance()`

### "Enroll fuel merchant"

Possible decomposition:

1. `login(...)`
2. `connectWallet(rpcUrl)`
3. `getContractFromRegistry("fuel merchant")`
4. `getSettlementWalletsForContract(contract.address)`
5. ensure merchant owner token and fee owner token exist
6. `merchantRegister(contract.address, merchantOwnerUserId, merchantOwnerPassword)`
7. verify returned settlement wallet information

### "Unlock fuel pump #10"

This is an intent, not a single PoB primitive.

Likely decomposition:

1. load the user session
2. ensure fuel merchant is enrolled
3. ensure sufficient balance
4. select the fuel merchant contract
5. `merchantHold(maxAuthorizedAmount)`
6. call the service-specific action that unlocks pump `#10`
7. later settle using the actual dispensed amount

PoB handles the payment authorization path. The physical or service action may require a separate service API outside `SPAAdapter`.

## Headless End-To-End Pattern

The local-repo file `test/adapter.hold_settle_bench.test.mjs` is the reference pattern for headless agent execution.

It does the following:

1. create customer and settlement adapter sessions
2. register and log in the customer
3. connect the wallet
4. load the merchant contract from the registry
5. inspect merchant and token owners
6. preload merchant-owner and fee-owner API tokens
7. link a bank account
8. cash in tokens
9. enroll the merchant via `merchantRegister(...)`
10. log in as the settlement child user
11. open a hold with `merchantHold(...)`
12. settle with `merchantSettle(...)`

Agents that can run Node should treat this test as executable guidance, not just documentation.

## Minimal Headless Example

```js
import { setupNodeRuntime } from "./node_runtime.mjs";

await setupNodeRuntime();

const { createSPAAdapter } = await import("./adapter.js");

const customerApi = createSPAAdapter({});
const settlementApi = createSPAAdapter({});

await customerApi.login({ userId: customerUserId, password: customerPassword });
await customerApi.connectWallet(rpcUrl);

const contract = await customerApi.getContractFromRegistry(contractName);
globalThis.__selectedContract = contract;
globalThis.__selectedContractName = contractName;

const ctx = await customerApi.getSettlementWalletsForContract(contract.address);

localStorage.setItem(
  "pob_api_tokens_by_owner_v1",
  JSON.stringify({
    [ctx.merchantOwner.toLowerCase()]: merchantOwnerToken,
    [ctx.feeOwner.toLowerCase()]: feeOwnerToken,
  }),
);

const enrollment = await customerApi.merchantRegister(
  contract.address,
  merchantOwnerUserId,
  merchantOwnerPassword,
);

const hold = await customerApi.merchantHold("25.00");

await settlementApi.login({
  userId: enrollment.settlementUserId,
  password: merchantOwnerPassword,
});
await settlementApi.connectWallet(rpcUrl);
globalThis.__selectedContract = contract;
globalThis.__selectedContractName = contractName;

const settled = await settlementApi.merchantSettle(hold.txId, "23.40");
```

## Approval Guidance

Before taking a write action, agents should distinguish:

Safe to do automatically:

- read contract state
- inspect enrollment state
- inspect balance
- inspect readiness

Require explicit approval unless already authorized by policy:

- registration of a new user
- bank linking
- cash-in / cash-out
- merchant enrollment
- opening a hold
- settling a payment

## Failure Modes To Check First

- no authenticated user
- no connected wallet
- no selected contract
- missing merchant owner token
- missing fee owner token
- merchant contract not registered in registry
- token contract not registered in registry
- settlement wallet not yet linked
- active account does not match the merchant settlement wallet during settlement

## What To Return

When completing a task, return the smallest useful operational summary:

- current user ID
- active address
- selected contract name and address
- whether the merchant is enrolled
- balance after funding or payment
- settlement user ID if enrollment created one
- hold `txId`
- hold and settle transaction hashes
- any remaining missing prerequisite

## Canonical Files

- `/llms.txt`
- `/pob-agent.json`
- `/docs/enroll-pob-chain.md`
- `/docs/js-api.md`
- `/skill.md`
- `/cli.mjs`
- `/node_runtime.mjs`

Local repo reference files:

- `test/adapter.hold_settle_bench.test.mjs`
- `test/node_runtime.mjs`
