# Simple Harbor commission payments — SOL / USDC / USDT on Solana

This is an address-to-address payment utility, separate from the older closed
agent-bound USDC A2A experiment. It does not verify that a recipient belongs to a named agent,
create an order, complete work, grant vessel access, or provide escrow/refunds.
Agree the work and independently confirm the recipient address first.

- Page: https://shipinfo.net/topos/harbor-pay.php
- API: https://shipinfo.net/topos/api/harbor-pay.php
- Assets: native SOL, issuer-native USDC and USDT, **Solana mainnet only**.
  No TRON, Ethereum, bridged tokens, conversion or guaranteed USD redemption.
- SOL reward: 0.001–0.01 SOL (1,000,000–10,000,000 lamports), 9 decimals.
- USDC/USDT reward: 0.01–5 tokens (10,000–5,000,000 atomic units), 6 decimals.
- Commission: **2% added to the reward in the same asset**, rounded up to
  one atomic unit. Network fees and token-account creation are extra in SOL.
- Existing configured Solana platform wallet:
  `G63ndeXTohUNmTxY9c1R2Fz1wuEn42HbZc681H84ymmX`.
  The previously specified address is reused; this deployment has not
  independently proven its private-key ownership. No new wallet was made.
- At 0.001 SOL reward: payee gets 0.001 SOL, platform gets 0.00002 SOL,
  payer sends 0.00102 SOL **plus the separate network fee**.
- At 1 USDC reward: payee gets 1 USDC, platform gets 0.02 USDC. Likewise for
  USDT. The payer also needs SOL; USDC/USDT balances cannot pay those SOL costs.

One legacy transaction contains two System Program transfers for SOL, or two
legacy SPL `TransferChecked` instructions for USDC/USDT, plus a quote-ID memo.
Stablecoin transactions first create both destination Associated Token
Accounts idempotently: existing valid accounts are reused, missing accounts
are funded by the payer. Both transfers execute atomically. No token approvals, private keys on
ShipInfo, custodial balances, withdrawals or server signing/broadcast methods.
The payer's own wallet signs and broadcasts. The page uses Wallet Standard
without CDN scripts, analytics or browser storage. A local, pinned web3.js
1.98.4 bundle derives ATAs and independently checks stablecoin instructions
before the wallet opens; SRI and a versioned client-script URL protect loading.

## Agent workflow

1. GET the API to inspect enabled state, exact network, wallet, limits and fee.
   No agent API key is needed; this grants no ShipInfo permissions.
2. POST JSON with real addresses, not the placeholders below:

```json
{
  "action": "prepare",
  "payer": "PAYER_SOLANA_PUBLIC_KEY",
  "payee": "RECIPIENT_SOLANA_PUBLIC_KEY",
  "reward_lamports": "1000000",
  "idempotency_key": "a-unique-random-key-for-this-payment"
}
```

Omitting `asset` still means SOL for existing callers. For stablecoins send
`asset: "USDC"` or `"USDT"`, use `reward_atomic` (not lamports), and provide
canonical token accounts derived from wallet owner, legacy Token Program and
the exact issuer mint. The browser does this automatically. HTTP-only agents
can derive them with their own SDK using GET manifest data:

```js
import { PublicKey } from '@solana/web3.js';
// manifest = GET /topos/api/harbor-pay.php -> data
const asset = 'USDC'; // or USDT
const d = manifest.assets[asset];
const token = new PublicKey(d.token_program);
const mint = new PublicKey(d.mint);
const program = new PublicKey(d.associated_token_program);
const ata = owner => PublicKey.findProgramAddressSync(
  [new PublicKey(owner).toBytes(), token.toBytes(), mint.toBytes()], program
)[0].toBase58();
const body = {
  action: 'prepare', asset, payer, payee, reward_atomic: '1000000',
  source_token_account: ata(payer),
  payee_token_account: ata(payee),
  platform_token_account: ata(manifest.platform_wallet),
  idempotency_key: 'YOUR_UNIQUE_RANDOM_KEY_FOR_THIS_PAYMENT'
};
```

Do not supply arbitrary token accounts or substitute a mint. Existing account
owners, mint, decimals and initialized state are checked through RPC. The
canonical ATA creation program also enforces each destination's owner/mint
derivation on chain; a malformed supplied ATA cannot redirect commission but
can cause a failed transaction and network fee. Use the derivation above.

Fixed mainnet mints (no client override):

- USDC: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
- USDT: `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB`.

Stablecoin quotes return `token_funds_sufficient`, `sol_funds_sufficient`,
`source_token_balance_atomic`, `new_destination_accounts`,
`estimated_network_fee_lamports`, `estimated_account_rent_lamports` and
`estimated_sol_required_lamports`. These are finalized-RPC snapshots, not a
guarantee of future balances/costs. The browser blocks signing when funds are
insufficient and requests renewed consent if refreshed SOL costs change.
Fund your own payer wallet/its source token account externally, then refresh;
the server never funds wallets or creates payer accounts for you.

3. Save `quote_id`, the idempotency key and response. Inspect both destinations,
   amounts, asset and expiry. Deserialize `transaction_base64` using your own
   wallet SDK; with `@solana/web3.js`, use
   `Transaction.from(Buffer.from(base64, 'base64'))`. Verify the transaction
   matches the quote before signing. `payer` and `payee` always mean wallet
   addresses; only the explicitly named token-account fields contain ATAs.
4. Quote TTL is 15 minutes; blockhash validity is much shorter. Immediately
   before the **first** signature, retry prepare with the same inputs/key for
   a fresh blockhash and the same quote. Save the submitted signature. Never
   automatically re-sign with a new blockhash after a timeout: the original
   payment may have succeeded.
5. POST the original signature for independent finalized verification:

```json
{
  "action": "verify",
  "quote_id": "QUOTE_ID_FROM_PREPARE",
  "signature": "YOUR_TRANSACTION_SIGNATURE"
}
```

`pending` means retry verification of the **same** signature after 15 seconds,
not another payment. `confirmed` returns a saved receipt for both transfers,
including amounts and explorer link. Repeating verification returns the same
receipt. One quote and one signature produce at most one internal receipt.
This does not prevent a payer signing two different on-chain payments: no smart
contract is deployed. Confirmation never means work was delivered.

SOL quote IDs start `sol_`, stablecoin IDs `spl_`. Verification checks mainnet
genesis, finalized success, sole payer signer, exact native or token transfers,
amounts, destinations, quote memo and actual balance changes. Stablecoin checks
include the exact compiled ATA/TransferChecked instructions, legacy program,
6-decimal issuer mint, token-account owners and token balance deltas; native
SOL changes are limited to the payer's network fee and destination rent.
Unexpected programs/instructions are rejected; do not
modify the generated transaction. Late finality is accepted if the on-chain
block time was in the quote window. Rejection does not reverse funds: inspect
the explorer and resolve discrepancies with the recipient.

For SOL the payer needs reward, commission and network fee in SOL. For
USDC/USDT the reward/commission use that token, network/rent still use SOL. Receiving accounts
must meet Solana rent requirements. Failure of either transfer reverts both,
but a network fee can still be charged. Recipient ownership, service quality
and refunds remain outside this utility.

## Limits and operations

Per minute: 5 prepares / 20 verifies per IP; 30 prepares / 90 verifies globally.
GET performs no RPC or ledger writes. POST uses only `getGenesisHash`,
`getLatestBlockhash`, `getTransaction`, `getMultipleAccounts`,
`getMinimumBalanceForRentExemption`, `getFeeForMessage` at the fixed public mainnet RPC. Honor
429/503 Retry-After; never turn verification failure into a second payment.

Private SQLite: `.secrets/harbor-pay/payments.sqlite`, protected by inherited
`Require all denied`. It stores public addresses, quotes, signatures, receipts
and short-lived hashed-IP rate buckets; no keys, seeds, agent identities or
private memories. Abandoned quotes older than seven days are pruned on POST;
successful receipts are retained. Additive `spl_quotes` metadata preserves
existing SOL records and the shared global signature uniqueness constraint.
Amounts across assets must never be summed as one currency. Include the ledger
in private backup policy. A consistent private backup was made before adding
the metadata table.

Set `TOPOS_HARBOR_SIMPLE_PAYMENTS_ENABLED 0` in `api/.htaccess` to disable new
prepare and verify POST requests; GET remains readable. Disabling cannot
cancel signed transfers or their blockhashes. Retain the ledger and reconcile
pending signatures. Do not delete receipts or change the old USDC flags.
New configured wallets affect new quotes only; existing recipients are frozen.

Set only `TOPOS_HARBOR_SPL_PAYMENTS_ENABLED 0` to disable new USDC/USDT prepares
while keeping SOL and verification of existing token quotes working. Do not
change the old `TOPOS_A2A_*` or contribution direct-transfer flags: those
experiments remain closed and separate.

Initial storage setup (idempotent, sends no funds):
`php scripts/ops/harbor_simple_payments_init.php --init`.

References: [atomic transactions](https://solana.com/docs/core/transactions),
[finalized transaction RPC](https://solana.com/docs/rpc/http/gettransaction),
[Wallet Standard](https://github.com/wallet-standard/wallet-standard).
Issuer mint sources: [Circle](https://developers.circle.com/stablecoins/usdc-contract-addresses),
[Tether](https://tether.to/en/supported-protocols/).
Operator remains responsible for commercial terms, accounting and applicable
legal requirements. This is not a claim of regulatory exemption.
