# CONTRACTS — MONEY, LAW, AND THE CODE THAT IMPLEMENTS BOTH
### Research pass. v0.1 — 2026-08-19. Companion to ORACLE-OS-CONTRACTS-QUESTS-GOAL.md

> "find legal things and other methods and sections and parts and Code to be able to make this
> working safe for both me and them and also let it work Great for both. at first they do the job
> they get paid at the end. it also should show them how much that will pay out and how long it
> will take and more data"

**I am not a lawyer or an accountant and none of this is legal, tax or financial advice.** Every
citation below is to a primary source or a firm's published analysis, dated, so you can hand this
to someone who *is* one and get an answer quickly instead of paying them to start from zero. The
sections marked **COUNSEL** are the ones to take to them first.

---

## 1. THE ONE ARCHITECTURAL DECISION EVERYTHING ELSE HANGS OFF

**Do not hold the money yourself.** The trigger for money-transmitter licensing is custody: the
moment your platform receives a buyer's money and later pays it to a worker, you are holding funds
that belong to someone else, and that activates licensing requirements in most US states.
Migrating architecture later, at volume, creates retroactive exposure *and* forces you to
re-onboard every single worker — which is why this is a before-you-launch question.

Four known structures, and where Panda Productions lands:

| Structure | What it means | Fit here |
|---|---|---|
| Merchant of record | You buy from the worker and sell to the poster. No transmission. You absorb every chargeback and quality dispute. | Partial — see §2 |
| **Payment facilitator** | A **licensed provider** holds the funds and settles to workers as sub-merchants. You orchestrate; you never take custody. | **This one** |
| Agent-of-the-payee | You are legally the worker's agent for receiving payment. Exempt in *some* states. | Supporting argument, varies by state — **COUNSEL** |
| Direct settlement | Buyer pays worker's own merchant account; you bill separately. No custody, fragmented experience. | No — breaks escrow |

Risk factors that raise exposure even inside the facilitator model, and each maps to a rule later
in this document: **holding worker balances or wallets** (don't — §3.4), **long payout delays**
(the escrow window is the thing to minimise — §3.3), **payout-detail changes** (the single
highest-risk event, usually account takeover — §5.2), and **absorbing chargebacks** when a worker
has no balance to cover them (§4.3).

---

## 2. THE PAYMENT ARCHITECTURE — STRIPE CONNECT, SEPARATE CHARGES AND TRANSFERS

Stripe Connect offers three charge types. For this board there is only one candidate.

| Charge type | Funds flow | Why not / why |
|---|---|---|
| Direct charge | Customer pays the connected account directly | No escrow window at all. The worker isn't even known at publish. |
| Destination charge | Charge on platform, funds transfer **immediately** | "Immediately" is the problem — there is no hold. |
| **Separate charges and transfers** | Charge on platform; transfers created **later**, to **one or many** accounts | **This.** It is the only type where the recipient can be unknown at charge time, and the only one that splits one payment across several accounts. |

Stripe's own words: separate charges and transfers exist "to transfer funds from one payment to
multiple connected accounts, **or when a specific user isn't known at the time of the payment**."
That is the Contracts flow and the Adventure pool, described exactly.

### 2.1 WHAT THIS COSTS YOU

With this charge type **you are the merchant of record**, and "your account balance gets debited
for the cost of the Stripe fees, refunds, and chargebacks." That is a real liability and it must
be priced into the platform fee, not discovered later.

### 2.2 THE TWO PARAMETERS THAT MATTER

- **`transfer_group`** — a string tying a charge and its transfers together. Use the contract id.
  Stripe is explicit that it "only identifies associated objects. It doesn't affect any standard
  functionality." It is for your reconciliation, not for control.
- **`source_transaction`** — this one *is* control. By default a transfer fails if it exceeds your
  available balance, and **Stripe does not retry failed transfers**. Setting `source_transaction`
  to the originating charge makes the transfer request always succeed and simply wait until those
  specific funds have settled. **Use it on every transfer.** Without it, a payout can fail
  silently because an unrelated automatic payout drained the balance an hour earlier.

Stripe also warns that automatic payouts on the platform account can interfere with transfers that
lack a `source_transaction` — so set the platform's payout schedule deliberately.

### 2.3 ACCOUNT SETUP

- **Embedded onboarding** — the worker never leaves the Oracle. Stripe owns the flow logic and
  supports 46+ countries; you own the theming. API onboarding is explicitly not recommended by
  Stripe ("resource-intensive… requires regular updates").
- **Express dashboard** for workers — payouts and payment activity, no developer surface. Note
  that **dashboard type is immutable**: changing it later means creating a new Account object, so
  decide once.
- **Negative-balance liability: start with Stripe responsible.** Stripe monitors risk, intervenes,
  and pursues negative balances. Taking that on yourself requires "both the operational team and
  the engineering resources" — which is a second business, not a feature.

**COUNSEL:** even inside this model, holding funds on the platform balance between charge and
transfer is precisely the custody window §1 is about. Stripe has a private-preview "funds
segregation" feature that "keeps payment funds in a protected holding state before you transfer
them" for this reason — worth asking your account manager about, and worth raising with counsel
alongside the agent-of-payee question.

---

## 3. ESCROW — "THEY DO THE JOB, THEY GET PAID AT THE END"

### 3.1 THE TIMELINE

```
  publish        accept        submit        poster accepts        settle
     │             │              │            (or auto-accept)      │
     ▼             ▼              ▼                  ▼               ▼
  CHARGE the    contract      work is        review window       TRANSFER to
  poster        assigned      delivered      closes              worker(s)
  ─────────────────────── funds on platform balance ───────────  ──────────►
                        ← minimise this window →                 payout to bank
```

### 3.2 CHARGE AT PUBLISH, NOT AT ACCEPT

A worker must never finish a job and *then* discover the money was never there. The contract does
not go OPEN until the charge has succeeded. The board shows an explicit **Funded** state, and an
unfunded contract is not visible to workers at all.

Use an immediate capture, not a manual-capture authorisation hold: card authorisations expire in
about a week, and Jobs and Missions run longer than that.

### 3.3 KEEP THE WINDOW SHORT

Every day funds sit on your balance is a day of custody. So:

- an **auto-accept timer** on SUBMITTED (proposed: 7 days) — a poster who goes quiet must not be
  able to hold a worker's money hostage;
- transfer the moment the work is ACCEPTED_WORK, not on a batch schedule;
- a hard cap on how long a contract may sit ACCEPTED without work starting, after which it expires
  and refunds.

### 3.4 NO WALLETS

There is no Panda Productions balance. Money is with the processor, or it is with a person. A
platform-held balance is the exact thing §1 says not to build, and it converts a facilitator model
back into a custody model with no other change.

---

## 4. THE CODE

Sketches, not final. Naming matches §2.3 of the goal doc.

### 4.1 FUND AT PUBLISH

```js
// POST [internal endpoint]   — owner/poster only
// The contract does not become OPEN until this charge has SUCCEEDED.
const intent = await stripe.paymentIntents.create({
  amount:   c.rewardCents + platformFeeCents(c),   // poster pays reward + fee
  currency: 'usd',
  customer: poster.stripeCustomerId,
  payment_method: poster.defaultPaymentMethodId,
  off_session: true,
  confirm: true,
  transfer_group: `contract_${c.id}`,              // ties charge -> later transfers
  metadata: { contractId: c.id, kind: c.kind, posterId: poster.id },
}, {
  // The contract id IS the idempotency key: a retried publish can never
  // charge twice, however the client behaved.
  idempotencyKey: `publish_${c.id}`,
});
// Do NOT flip to OPEN here. The webhook does that (§4.4) — the client can die
// between the API call and the response, and the truth is Stripe's, not ours.
await contracts.patch(c.id, { escrow: { intentId: intent.id, state: 'pending' } });
```

### 4.2 RELEASE — ONE WORKER, AND THE ADVENTURE POOL

```js
// Called only from the ACCEPTED_WORK transition, server-side, never from a route
// the client can reach directly.
async function releaseEscrow(assignment) {
  const c      = await contracts.get(assignment.contractId);
  const charge = c.escrow.chargeId;                       // from the webhook
  const splits = payoutSplits(c, assignment);             // [{userId, cents}]

  const total = splits.reduce((n, s) => n + s.cents, 0);
  if (total > c.rewardCents) throw new Error('split exceeds escrowed reward');

  const results = [];
  for (const s of splits) {
    const worker = await panda.profile(s.userId);
    if (!worker.stripeAccountId || !worker.payoutsEnabled) {
      // Not a failure of the work — a failure of onboarding. Park it, tell them,
      // and retry when the account turns on. Never silently drop a payout.
      await payouts.park(assignment.id, s.userId, s.cents, 'payouts_not_enabled');
      continue;
    }
    const t = await stripe.transfers.create({
      amount: s.cents,
      currency: 'usd',
      destination: worker.stripeAccountId,
      transfer_group: `contract_${c.id}`,
      // THE IMPORTANT ONE. Without it a transfer fails outright when the platform
      // balance is short, and Stripe does not retry.
      source_transaction: charge,
      metadata: { assignmentId: assignment.id, userId: s.userId, kind: c.kind },
    }, {
      // Per assignment AND per user, so an Adventure's five transfers are five
      // distinct idempotent operations rather than one that can half-apply.
      idempotencyKey: `payout_${assignment.id}_${s.userId}`,
    });
    results.push({ userId: s.userId, transferId: t.id, cents: s.cents });
  }
  await ledger.append(c.id, 'PAID', { splits: results, at: Date.now() });
  return results;
}
```

```js
// Adventures: the pool split. The formula is frozen at publish and stored ON the
// contract, so what settles is what people agreed to when they joined — not
// whatever the code happens to say months later.
function payoutSplits(c, a) {
  if (c.kind !== 'adventure') return [{ userId: a.holderId, cents: c.rewardCents }];

  const scored = a.contributions.reduce((m, x) => {
    m[x.userId] = (m[x.userId] || 0) + x.weight; return m;
  }, {});
  const totalW = Object.values(scored).reduce((n, w) => n + w, 0);
  if (!totalW) return [];                               // nobody contributed: refund

  const floor = c.pool.minShareCents || 0;              // "who contributed and who didn't"
  const parts = Object.entries(scored)
    .filter(([, w]) => w > 0)
    .map(([userId, w]) => ({ userId, cents: Math.max(floor, Math.floor(c.rewardCents * w / totalW)) }));

  // Integer money: hand the rounding remainder to the largest contributor rather
  // than letting cents evaporate. The sum must equal the pool exactly.
  const drift = c.rewardCents - parts.reduce((n, p) => n + p.cents, 0);
  if (drift !== 0) parts.sort((x, y) => y.cents - x.cents)[0].cents += drift;
  return parts;
}
```

### 4.3 REFUND AND REVERSAL

```js
// ABANDONED / EXPIRED / dispute resolved for the poster.
await stripe.refunds.create(
  { payment_intent: c.escrow.intentId, reason: 'requested_by_customer' },
  { idempotencyKey: `refund_${c.id}` }
);
// If money has already moved, claw the transfer back FIRST, then refund. A refund
// with the transfer still out leaves the platform balance negative — and with
// separate charges and transfers, that is your balance, not the worker's.
await stripe.transfers.createReversal(transferId, { amount: cents },
  { idempotencyKey: `reverse_${assignmentId}_${userId}` });
```

### 4.4 WEBHOOKS ARE THE SOURCE OF TRUTH

The client can close the tab, the phone can lose signal, and some payment methods take 2–14 days
to confirm. **Never advance the state machine from a client callback.**

```js
// POST [internal endpoint]   — signature-verified, before any auth gate
switch (event.type) {
  case 'payment_intent.succeeded': {
    const pi = event.data.object;
    const id = pi.metadata.contractId;
    // Idempotent by construction: setting OPEN twice is setting OPEN.
    await contracts.transition(id, 'OPEN', {
      escrow: { state: 'funded', chargeId: pi.latest_charge, intentId: pi.id },
      actor: 'stripe', evt: event.id,
    });
    break;
  }
  case 'payment_intent.payment_failed':
    await contracts.transition(event.data.object.metadata.contractId, 'DRAFT',
      { reason: 'funding_failed', actor: 'stripe', evt: event.id });
    break;
  case 'account.updated': {
    // payouts_enabled flipping ON is the trigger to retry every parked payout
    // for that worker. This is the only path that unsticks them.
    const acct = event.data.object;
    if (acct.payouts_enabled) await payouts.retryParked(acct.id);
    await panda.syncVerification(acct);
    break;
  }
  case 'transfer.reversed':
  case 'charge.dispute.created':
    await ledger.append(/* … */); await ops.alert(event);
    break;
}
```

Store `event.id` and reject duplicates — Stripe retries, and a replayed
`payment_intent.succeeded` must not re-run anything with a side effect.

### 4.5 THE STATE MACHINE IS A TABLE, NOT SCATTERED `if`s

```js
// One place where every legal move is written down. Anything not in here cannot
// happen, and "who is allowed" is data rather than a condition buried in a route.
const MOVES = {
  DRAFT:         { publish: { to: 'OPEN',          by: 'poster', guard: 'fundsCharged' } },
  OPEN:          { apply:   { to: 'APPLIED',       by: 'worker', guard: 'meetsRequirements' },
                   cancel:  { to: 'EXPIRED',       by: 'poster', effect: 'refund' } },
  APPLIED:       { approve: { to: 'ACCEPTED',      by: 'poster' },
                   reject:  { to: 'REJECTED',      by: 'poster', needs: 'reason' } },
  ACCEPTED:      { start:   { to: 'IN_PROGRESS',   by: 'worker' },
                   abandon: { to: 'ABANDONED',     by: 'worker', effect: 'refund' } },
  IN_PROGRESS:   { submit:  { to: 'SUBMITTED',     by: 'worker' } },
  SUBMITTED:     { accept:  { to: 'ACCEPTED_WORK', by: 'poster|timer', effect: 'release' },
                   revise:  { to: 'REVISIONS',     by: 'poster', needs: 'reason' },
                   dispute: { to: 'DISPUTED',      by: 'poster|worker', needs: 'reason' } },
  REVISIONS:     { submit:  { to: 'SUBMITTED',     by: 'worker' } },
  ACCEPTED_WORK: { settle:  { to: 'PAID',          by: 'system' } },
  DISPUTED:      { resolve: { to: 'PAID|EXPIRED',  by: 'admin', needs: 'reason' } },
};
```

Every applied move appends `{ from, to, actor, at, reason, evt }` to an append-only ledger. With
real money, that log is what a dispute is decided on, and it only exists if it was written before
anyone needed it.

---

## 5. VERIFICATION, FRAUD, AND THE THINGS THAT ACTUALLY GO WRONG

### 5.1 TIERED, NOT ALL-AT-ONCE

Industry practice for facilitator models is light verification to browse and take low-value work,
with progressive verification triggered by **volume, payout amount, or risk signals**. Stripe's
embedded onboarding collects identity; you gate the *board* on tier. Practically: a worker can
browse and even work before payout onboarding is finished — but the profile must say clearly and
early that they cannot be **paid** until it is, not at the moment of payout.

### 5.2 PAYOUT-DETAIL CHANGES ARE THE HIGHEST-RISK EVENT

A changed bank account is the classic account-takeover signature. Require re-authentication, cool
off pending payouts for a short window, and notify the old contact method as well as the new one.

### 5.3 ONGOING MONITORING

Dispute rates, refund patterns, sudden volume changes, and **linkage signals across supposedly
independent accounts** — the same device, card, or bank behind two "different" workers bidding on
the same contract.

---

## 6. TAX — WHO FILES WHAT

**COUNSEL / CPA.** The numbers below are current as of August 2026 and are the ones to confirm.

### 6.1 WHO ISSUES THE FORM DEPENDS ON A SINGLE STRIPE FIELD

Stripe issues 1099-K forms only where **Stripe** controls pricing or the connected account pays
Stripe's fees directly — i.e. `controller.fees.payer` is `account`. Where
`controller.fees.payer` is `application` (the platform controls pricing, which is this design),
**Stripe does not issue a 1099-K and the platform is responsible for filing.** Stripe sells a
1099 tax-reporting product that files them for you; using it is strongly advisable over building
it.

### 6.2 THE FORM AND THE THRESHOLD

For a marketplace paying independent workers for services, the form is **1099-NEC** (non-employee
compensation), not 1099-K.

| Form | Tax year 2025 | Tax year 2026 onward |
|---|---|---|
| 1099-NEC / 1099-MISC | $600 | **$2,000** (OBBBA) |
| 1099-K (TPSO) | phased/disputed | **$20,000 AND 200 transactions**, permanently restored |

Note a live discrepancy: **Stripe's own Connect tax-reporting page still states $600 for
1099-NEC**, which was the pre-OBBBA threshold. Over-reporting is not an error — filing at $600
when the statutory floor is $2,000 is safe, while the reverse is not. Collect a **W-9 from every
worker at onboarding regardless**, because it is also what supports backup withholding if a TIN is
missing or wrong, and chasing it at year end is miserable.

### 6.3 WORKER CLASSIFICATION

**COUNSEL.** As of August 2026 this is genuinely unsettled: the 2024 DOL rule remains in effect
for private litigation while the DOL has stopped enforcing it; a proposed 2026 rule (published
27 Feb 2026, comments closed 28 Apr 2026) would restore the 2021 standard and is **not final**.
The proposal weights two core factors — **control over the work** and **opportunity for profit or
loss** — and emphasises actual practice over what a contract says.

And critically: **none of this touches state law.** California's ABC test and other state
standards apply independently.

What that means for the product, concretely: the further the platform goes toward setting
schedules, dictating method, or preventing workers from declining work or working elsewhere, the
worse the classification position gets — regardless of what the terms say, because the 2026
proposal explicitly looks at practice over paper. Quests and Missions letting workers choose
assignments and set their own approach is not just nicer, it is the defensible design.

---

## 7. WHAT THE WORKER SEES BEFORE THEY ACCEPT

> "it also should show them how much that will pay out and how long it will take and more data"

Everything below is known at publish, so all of it can be on the card. The rule: **the number
shown biggest is the number that lands in their bank.**

| Field | Why it earns its place |
|---|---|
| **Net to you** | Reward minus platform fee, in bold. Not the gross. |
| Gross / fee breakdown | One line under it, so the deduction is never a surprise |
| **Funded ✓** | Escrow state, from Stripe. The single most trust-building thing on the card |
| Estimated time | Poster's estimate, **plus the median actual** from completed contracts of that kind — the second number is the honest one |
| Deadline | Absolute date, and time remaining |
| Payout timing | "Released on acceptance · in your bank in N days" — includes the review window and the Stripe payout schedule, not just the transfer |
| Auto-accept | "Auto-accepts N days after you submit if the poster doesn't respond" |
| Kind + tier | Task / Quest / Mission / Adventure / Job, and the skill floor |
| Party | Slots filled / total; for Adventures, the **live split preview** |
| Poster record | Contracts posted, acceptance rate, average time-to-review, disputes |
| Requirements | Level or trade needed, up front — not discovered on the application form |
| Renewal | Jobs only: term length and renewal history |

For Adventures add a **live share preview** — everyone in the party sees their current cut as they
work. The worst possible version of that feature is people finding out at settlement.

---

## 8. WHAT TO ASK COUNSEL, IN ORDER

1. Given a Stripe Connect facilitator model with funds resting on the platform balance between
   charge and transfer — do we need money-transmitter licensing in our target states, and does the
   agent-of-the-payee exemption apply to us? (§1, §2)
2. Whose contract is the work performed under — ours, or poster-to-worker with us as venue? This
   decides the ToS, the dispute process, and most of the classification exposure.
3. Independent-contractor classification given the five kinds, under the current federal position
   *and* the states we operate in. (§6.3)
4. 1099-NEC filing obligations and thresholds for TY2026, and whether Stripe's product satisfies
   them. (§6.1, §6.2)
5. Terms of service, dispute resolution, and prohibited-work policy.
6. Whether the platform fee changes any of the above.

---

## 9. SOURCES

- Stripe — *Accept a payment using separate charges and transfers*: https://docs.stripe.com/connect/marketplace/tasks/accept-payment/separate-charges-and-transfers
- Stripe — *Design an advanced Connect integration*: https://docs.stripe.com/connect/design-an-integration
- Stripe — *US tax reporting for Connect platforms*: https://docs.stripe.com/connect/tax-reporting
- HL Hunt — *Marketplace Payments: Paying Sellers Without Becoming a Money Transmitter*: https://www.hlhunt.org/uncategorized/marketplace-payments-paying-sellers-without-becoming-a-money-transmitter/
- Pease Bell CPAs — *The OBBBA Act: Form 1099 Reporting*: https://www.peasebell.com/insights/the-obbba-act-form-1099-reporting/
- Jackson Lewis — *DOL's Proposed 2026 Independent Contractor Rule*: https://www.jacksonlewis.com/insights/dols-proposed-2026-independent-contractor-rule-what-employers-need-know
- US DOL — *Employee or Independent Contractor Classification Under the FLSA*: https://www.dol.gov/agencies/whd/flsa/misclassification/rulemaking
- Faisal Khan & Co — *Agent of Payee Exemption*: https://faisalkhan.com/solutions/licensing/money-transmitter-license/agent-of-payee-exemption
