Skip to content
6 changes: 3 additions & 3 deletions docs/PAYRAM_HEADLESS_AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Run from repo root: `./setup_payram_agents.sh [command]`
| `status` | Check API reachable and auth (token saved / valid) |
| `setup` | First-time: register root user + create default project |
| `signin` | Sign in; saves token to `.payraminfo/headless-tokens.env` |
| `ensure-config` | Seed `payram.frontend` and `payram.backend` for local API (needed for payment creation) |
| `ensure-config` | Ensure the canonical server URL (`payram.server.url` via `/system/site-url`) is set — payment-link creation 500s without it. Falls back to the legacy `payram.frontend`/`payram.backend` keys on older cores |
| `ensure-wallet` | Create random BTC wallet or link existing to project (for payment links) |
| `deploy-scw` | Deploy ETH/EVM smart-contract deposit wallet; then auto-link to project |
| `deploy-scw-flow` | Generate mnemonic -> fund deployer -> balance check -> deploy SCW |
Expand All @@ -133,7 +133,7 @@ Set these for non-interactive or scripted runs. For agents, prefer env-driven, n
| `PAYRAM_PAYMENT_EMAIL` | — | Customer email for payment link |
| `PAYRAM_PAYMENT_AMOUNT` | `10` | Amount in USD for payment link |
| `PAYRAM_CUSTOMER_ID` | from signin | Usually from token file after signin |
| `PAYRAM_FRONTEND_URL` | `http://localhost` | Used by ensure-config (local) |
| `PAYRAM_FRONTEND_URL` | `http://localhost` | Used by ensure-config's legacy fallback only (older cores; current cores derive the URL from the request) |
| `PAYRAM_NETWORK` | `mainnet` | One-step flow network selection (`mainnet` default - real payments; `testnet` to try with free coins) |
| `PAYRAM_NODE_MODE` | `docker` | JS runtime: `docker` or `host` |
| `PAYRAM_NODE_DOCKER_IMAGE` | `node:20-bullseye-slim` | Docker image used for JS scripts |
Expand Down Expand Up @@ -213,7 +213,7 @@ The one-step flow does:
2. Install or restart PayRam using `setup_payram.sh` (fresh install asks its one-time questions in the terminal; see Prerequisites).
3. Re-reads `config.env` and waits for API readiness at the real port.
4. Auth (`setup` if no root user, else `signin`).
5. `ensure-config` for local frontend/backend settings.
5. `ensure-config` so the server URL (`payram.server.url`) is set for payment links.
6. Wallet flow (MVF: **USDC on Base**):
- Default: EVM smart-contract wallet deploy (blocking, guided gas funding).
The master (deployer) wallet is generated locally and is **ops-only** -
Expand Down
34 changes: 23 additions & 11 deletions scripts/deploy-scw-eth.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ async function main() {
const factoryAddress = factoryData.address;
const factoryId = factoryData.id;
const abi = typeof factoryData.abi === 'string' ? JSON.parse(factoryData.abi) : factoryData.abi;
if (!factoryAddress || !abi) {
console.error('Factory contract missing address or abi');
// factoryId is part of the registration URL after the deploy tx — validate
// it here so a malformed response can't burn gas and then POST to /undefined.
if (!factoryAddress || !abi || factoryId == null || String(factoryId).trim() === '') {
console.error('Factory contract missing address, abi, or id');
process.exit(1);
}
console.log('Factory contract OK:', factoryAddress);
Expand All @@ -99,6 +101,18 @@ async function main() {
const provider = new ethers.JsonRpcProvider(PAYRAM_ETH_RPC_URL);
const signer = wallet.connect(provider);

// Resolve the project BEFORE paying gas: current core registers SCWs only
// on the project-scoped route (the unscoped legacy route no longer exists),
// so an empty project id would burn the deploy gas and then fail to
// register the wallet with PayRam.
const projectId = (process.env.PAYRAM_PROJECT_ID || '').trim();
if (!projectId) {
console.error(
'PAYRAM_PROJECT_ID is not set - refusing to deploy. The tx would spend gas on-chain but SCW registration with PayRam would fail. Set PAYRAM_PROJECT_ID (the script normally resolves it via get_first_project_id) and retry.'
);
process.exit(1);
}

// 3) Salt bytes32 (same idea as frontend: keccak256 of random)
const salt = ethers.keccak256(ethers.toUtf8Bytes(`${Date.now()}-${Math.random().toString(36).slice(2)}`));

Expand Down Expand Up @@ -143,17 +157,15 @@ async function main() {
// 5) Register with backend. The route moved across core versions:
// current core: POST /api/v1/project/{projectID}/wallets/deposit/scw/blockchains_contract/{id}
// older images: POST /api/v1/wallets/deposit/scw/blockchains_contract/{id}
// Body ({name, transactionHash}) is the same in both. Try the
// project-scoped path when we know the project, fall back to legacy on
// 404/405 so the script works against whichever image is deployed.
const projectId = (process.env.PAYRAM_PROJECT_ID || '').trim();
// Body ({name, transactionHash}) is the same in both. projectId was
// validated before the deploy tx, so the scoped path is always first;
// legacy stays as a 404/405 fallback for older images.
const registerBody = JSON.stringify({ name: PAYRAM_SCW_NAME, transactionHash: txHash });
const registerHeaders = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` };
const candidates = [];
if (projectId) {
candidates.push(`${PAYRAM_API_URL}/api/v1/project/${projectId}/wallets/deposit/scw/blockchains_contract/${factoryId}`);
}
candidates.push(`${PAYRAM_API_URL}/api/v1/wallets/deposit/scw/blockchains_contract/${factoryId}`);
const candidates = [
`${PAYRAM_API_URL}/api/v1/project/${projectId}/wallets/deposit/scw/blockchains_contract/${factoryId}`,
`${PAYRAM_API_URL}/api/v1/wallets/deposit/scw/blockchains_contract/${factoryId}`,
];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// candidates always has at least the legacy URL, so registerRes is always set.
let registerRes;
Expand Down
47 changes: 43 additions & 4 deletions setup_payram_agents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,16 @@ print(' '.join(x['code'] for x in d if str(x.get('status','active')).lower()=='a

res=$(api GET "/api/v1/system/workers/status" "" true)
parse_response "$res"
workers_body="$HTTP_BODY"
# A host without supervisord answers 500 here; without this guard every
# chain would falsely degrade to listener-down with a bogus restart hint.
local workers_available="yes"
if [[ "$HTTP_CODE" != "200" ]]; then
workers_available="no"
workers_body='{"status":[]}'
echo "Note: worker status unavailable (HTTP $HTTP_CODE) - listener state unknown; RPC/block-age checks still run."
else
workers_body="$HTTP_BODY"
fi

echo "Node sync status (thresholds: 10m, BTC 90m):"
local issues=0 chain
Expand Down Expand Up @@ -1049,7 +1058,9 @@ elif age>=0:
else:
print('healthy no-timestamp')" 2>/dev/null || echo "unreachable parse-failed") || true
fi
[[ "$verdict" == "healthy" && "$listener_up" == "no" ]] && verdict="listener-down"
# Only assert listener-down when worker status was actually available.
[[ "$workers_available" == "yes" && "$verdict" == "healthy" && "$listener_up" == "no" ]] && verdict="listener-down"
[[ "$workers_available" == "no" ]] && listener_up="unknown"

local flag="✓"
[[ "$verdict" == "lagging" ]] && flag="⚠"
Expand Down Expand Up @@ -1270,11 +1281,37 @@ cmd_signin() {
ensure_config() {
ensure_token || return 1
local base_url="${PAYRAM_API_URL}"
local res
# Current cores keep ONE canonical server URL (payram.server.url), set via
# /system/site-url; the old payram.frontend/payram.backend keys are
# hard-deleted on boot, so writing them is a silent no-op and payment-link
# creation 500s while payram.server.url is unset. GET always returns 200,
# with {"siteUrl":null} when the key is absent.
res=$(api GET "/api/v1/system/site-url" "" true)
parse_response "$res"
if [[ "$HTTP_CODE" == "200" ]]; then
if echo "$HTTP_BODY" | grep -q '"siteUrl":[[:space:]]*null'; then
# POST takes no URL — core derives the origin from THIS request.
# Only the scheme is accepted in the body; pass it explicitly so
# an https install behind a proxy is not downgraded to http.
local scheme="http"
[[ "$base_url" == https://* ]] && scheme="https"
res=$(api POST "/api/v1/system/site-url" "{\"scheme\":\"$scheme\"}" true)
parse_response "$res"
case "$HTTP_CODE" in
200) echo "Set server URL (payram.server.url) from ${base_url}" ;;
403) echo "Server URL is unset and this token is not root — sign in as the root member and re-run ensure-config, or payment-link creation will fail." ; return 1 ;;
*) echo "Warning: could not set server URL (HTTP $HTTP_CODE) — payment-link creation may fail until it is set." ; return 1 ;;
esac
fi
return 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi
# Older cores (no /system/site-url): fall back to the legacy split keys,
# which only ever mattered on localhost installs.
local frontend_url="${PAYRAM_FRONTEND_URL:-http://localhost}"
if [[ "$base_url" != *"localhost"* && "$base_url" != *"127.0.0.1"* ]]; then
return 0
fi
local res
res=$(api GET "/api/v1/configuration/key/payram.frontend" "" true)
parse_response "$res"
if [[ "$HTTP_CODE" == "404" || "$HTTP_CODE" == "500" ]] || ! echo "$HTTP_BODY" | grep -q '"key"'; then
Expand Down Expand Up @@ -2609,7 +2646,9 @@ flow_main() {
fi

log "Ensuring config..."
ensure_config
# Tolerated failure: the rest of the flow (wallets, gas) is still useful;
# ensure_config already printed why payment-link creation would fail.
ensure_config || log "Config warning: server URL not set — payment-link creation may fail (see above)."

if [[ "$setup_mode" == "operator" ]]; then
log "Operator lane: configuring fee collectors + default fees..."
Expand Down
6 changes: 4 additions & 2 deletions updater-configs/upgrade-policy.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"latest": "3.5.0",
"latest": "3.5.2",
"updater_api_init_version": "1.9.6",
"arch_support": {
"arm64": "1.9.1"
Expand All @@ -12,6 +12,8 @@
}
],
"releases": [
"3.5.2",
"3.5.1",
"3.5.0",
"3.4.0",
"3.3.0",
Expand Down Expand Up @@ -75,5 +77,5 @@
"1.0.1",
"1.0.0"
],
"dashboard_max": "3.5.0"
"dashboard_max": "3.5.2"
}