Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/settlement/LIVE_EVIDENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Live settlement evidence

B03 handoff artifact.

## Status

`LIVE_NOT_RUN`

No real Arc Testnet settlement has been executed. No Privy application,
execution wallet, policy, or funded testnet account has been provisioned for
this build.

This is the expected state. B03 closes on its offline criteria, and
`milestones/coder-b/B03-live-settlement-harness.md` states that live
availability does not block packet closure. Live execution is tracked as
project Gate P4 evidence.

## Why it has not run

Provisioning requires a human: creating a Privy application, holding an app
secret, attaching a wallet policy, and funding a testnet account are all
actions an agent must not perform. `docs/settlement/PROVIDER_SETUP.md` is the
procedure; nobody has run it yet.

## What is proven without it

The offline harness exercises the complete adapter workflow against simulated
providers with a deterministic broadcast counter:

- Every policy denial family produces **zero** external broadcasts.
- Duplicate delivery, ten sequential retries, and ten parallel workers sharing
durable state each produce **exactly one** broadcast.
- Process restart is exercised against file-backed durable state: an attempt is
written to disk before the provider is called, and a restarted worker reading
that file is refused a second submission right, including after an outcome
that was never learned.
- An ambiguous outcome does not grant a fresh submission right, so the
dangerous retry after a possible payment cannot happen.
- Every ambiguous or unrecognized provider response classifies as
`POSSIBLY_SUBMITTED`.
- Sanitized fixtures reproduce the same verifier and classifier results as the
raw responses they were captured from.

Command:

```bash
cd packages/testkit-settlement && npm run check
```

## What is not proven

Simulators prove the adapter's logic, not the provider's behaviour. Still
unverified against reality:

- That a Privy policy configured as `buildExpectedPolicy` describes actually
denies each wrong dimension. The policy shape is modelled from Privy's
documentation, not observed.
- That Arc Testnet receipts and Transfer logs have the exact shape the verifier
expects.
- That the documented Privy wallet and policy identifier formats match the
conservative shape check in the readiness probe.
- Real latency, rate limits, and error bodies.

Per `.agents/skills/sponsor-qualification/SKILL.md`, no sponsor or
qualification claim may be made from fixtures alone. Until this file records a
sanitized live transaction, the Privy and Arc integration claims are
`NOT VERIFIED`.

## To update this file

Run `docs/settlement/PROVIDER_SETUP.md`, execute one allowed settlement, then
replace the status above with `LIVE_RUN` plus the sanitized transaction hash,
block number, explorer URL, and the observed denial counts. Capture the
responses through `captureReceiptFixture` so no credential reaches the
repository.
133 changes: 133 additions & 0 deletions docs/settlement/PROVIDER_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Provider setup (human-run)

B03.1. Prepares a Privy application, execution wallet, policy, and Arc Testnet
funding so a real settlement can be captured as Gate P4 evidence.

**A human runs every step here.** Agents must not create accounts, enter
credentials, mutate policy, or move funds. An agent may run the read-only
verification in section 7 and report results.

Testnet only. Nothing in this guide applies to mainnet, and Arc mainnet
parameters are unpublished. See `docs/settlement/SETTLEMENT_CONFIG_V1.md`.

## 1. Before you start

You need a Privy account, a terminal, and somewhere to store secrets that is
not this repository. Budget about 30 minutes.

At no point paste a secret into a chat, an issue, a PR, a fixture, a log, or a
review prompt. `.agent/SECURITY_INVARIANTS.md` treats that as a breach, and a
committed secret must be rotated, not deleted.

## 2. Privy application

1. Create a Privy application for OneShot.
2. Record the **app ID**. It is a public identifier, safe to log.
3. Create an **app secret**. This is the one true credential in the system.
Put it straight into your secret store; do not write it to a file first.

## 3. Execution wallet

1. Create a server wallet. This is the only wallet that will sign settlement.
2. Record the **wallet ID** and the **wallet address**.
3. Configure the owner or key quorum according to your organisation's rules. A
single-owner wallet is acceptable for a testnet demo and is not acceptable
for anything holding real value.

## 4. Recipient and cap

Two human decisions, both deliberate:

- **Recipient allowlist.** The addresses settlement may pay. Keep it as short
as the demo allows. An empty list settles nothing, which is the safe default.
- **Per-settlement cap.** The maximum atomic units for one settlement, in
six-decimal USDC atomic units. `1000000` is one USDC.

These are the two values that bound the blast radius if everything else fails.

## 5. Policy

Attach a policy to the execution wallet constraining all six dimensions:

| Dimension | Constraint |
| --- | --- |
| Chain | equals `5042002` |
| Destination contract | equals the USDC interface `0x3600000000000000000000000000000000000000` |
| Native value | equals `0` |
| Method | `transfer` |
| Recipient | in your allowlist |
| Amount | at or below your cap |

The policy must end with a **default deny**. Without it, anything the rules do
not mention is permitted.

`buildExpectedPolicy` in `@oneshot/privy-adapter` produces this shape, and
`policyDigest` produces a fingerprint you can compare against later to detect
drift. Record the **policy ID** and the digest.

## 6. Funding

1. Fund the execution wallet from the Circle faucet at
<https://faucet.circle.com> for Arc Testnet.
2. Fund only what the demo needs.
3. Confirm the balance on the explorer at <https://testnet.arcscan.app>.

Arc's native gas asset uses 18 decimals while the USDC ERC-20 interface uses 6.
They are both called USDC. Read balances carefully.

## 7. Verification (read-only, safe to automate)

Set the variables from `packages/arc-adapter/.env.example` in your shell or
secret store, then run the readiness probe:

```bash
cd packages/arc-adapter && npm run probe
```

This contacts the endpoint you configured, asks it which chain it is actually
on, and checks that the configured USDC address holds contract bytecode. It is
read-only: it cannot sign, send, or mutate anything, and it prints no
credential. Exit code 0 means ready, 1 means not ready.

Read the result carefully, because the two failure modes need opposite
responses:

- **`MISMATCH`** — the endpoint answered and the answer was wrong. Your
configuration points somewhere it should not. A human must fix it. Do not
retry; it will not resolve on its own.
- **`UNAVAILABLE`** — the endpoint could not be reached. Your configuration may
be perfectly correct. Retrying later is reasonable.

Note that `npm run check` is a different thing: it runs lint, typecheck, and
the unit tests against stubbed endpoints. It proves the probe's logic is
correct and tells you nothing about whether *your* setup is correct. Only
`npm run probe` does that.

## 8. Storing the values

| Value | Classification | Where it goes |
| --- | --- | --- |
| App ID | public | configuration |
| App secret | **secret** | secret store only |
| Wallet ID | public | configuration |
| Wallet address | public | configuration and evidence |
| Policy ID | public | configuration |
| Recipient allowlist | human-only | configuration |
| Cap | human-only | configuration |

Never commit a real value. `packages/arc-adapter/.env.example` holds
placeholders only and is generated from the config schema.

## 9. Cleanup and rotation

When the demo is finished:

1. Return or drain remaining testnet funds.
2. Rotate the app secret, and rotate immediately if it was ever pasted anywhere
outside the secret store.
3. Narrow or empty the recipient allowlist.
4. Keep the wallet and policy if you need the evidence trail; disable the
policy's allow rule to make the wallet inert.

Rotation is the response to a suspected leak. Deleting the message that
contained it is not.
5 changes: 5 additions & 0 deletions packages/arc-adapter/bin/probe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { runProbe } from '../dist/probe-cli.js';

const code = await runProbe(process.env);
process.exit(code);
8 changes: 5 additions & 3 deletions packages/arc-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
}
},
"files": [
"dist"
"dist",
"bin"
],
"scripts": {
"build": "tsc --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"lint": "eslint src test",
"test": "vitest run",
"test:watch": "vitest",
"check": "npm run lint && npm run typecheck && npm run test && npm run build"
"check": "npm run lint && npm run typecheck && npm run test && npm run build",
"probe": "npm run build && node bin/probe.js"
},
"dependencies": {
"viem": "2.56.3"
Expand All @@ -38,4 +40,4 @@
"typescript-eslint": "8.69.0",
"vitest": "5.0.0"
}
}
}
2 changes: 2 additions & 0 deletions packages/arc-adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from './config.js';
export * from './readiness.js';
export * from './receipt.js';
export * from './outcome.js';
export * from './viem-probe.js';
export * from './probe-cli.js';
63 changes: 63 additions & 0 deletions packages/arc-adapter/src/probe-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Read-only readiness command (B03.1 §7).
*
* Loads configuration from the environment, probes the configured Arc
* endpoint, and prints a per-check report. It performs no mutation, signs
* nothing, and prints no credential.
*
* This exists because `npm run check` runs unit tests against stubs. Those
* prove the probe's logic; they say nothing about whether a particular
* operator's endpoint, chain, and token are correct. Only this command does.
*
* Exit codes: 0 ready, 1 not ready.
*/

import { loadSettlementConfig } from './config.js';
import { probeReadiness } from './readiness.js';
import { createViemProbe } from './viem-probe.js';

const SYMBOL: Readonly<Record<string, string>> = {
PASS: 'PASS ',
MISMATCH: 'MISMATCH',
UNAVAILABLE: 'UNAVAIL',
SKIPPED: 'SKIP ',
};

export async function runProbe(env: NodeJS.ProcessEnv): Promise<number> {
let config;
try {
config = loadSettlementConfig(env);
} catch (error) {
// Configuration errors name the variable, never its value.
console.error(`Configuration error: ${(error as Error).message}`);
return 1;
}

console.log(`profile : ${config.profile.id} (chain ${config.profile.chainId})`);
console.log(`token : ${config.profile.tokenContract}`);
console.log(`settle : ${config.profile.tokenDecimals} decimals`);
console.log(`gas : ${config.profile.nativeDecimals} decimals`);
console.log('');

const report = await probeReadiness(config, createViemProbe(config));

for (const check of report.checks) {
console.log(`${SYMBOL[check.status] ?? check.status} ${check.name}: ${check.detail}`);
}

console.log('');
if (report.ready) {
console.log('READY');
return 0;
}

if (report.hasMismatch) {
// The distinction that matters to whoever is reading this output.
console.log('NOT READY: identity mismatch. A human must fix the configuration.');
console.log('Do not retry; a mismatch will not resolve on its own.');
} else {
console.log('NOT READY: endpoint unavailable. Configuration may be correct.');
console.log('Retrying later is reasonable.');
}
return 1;
}
Loading
Loading