← all documents · raw markdown · 22 KB

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

Stripe is explicit that it "only identifies associated objects. It doesn't affect any standard

functionality." It is for your reconciliation, not for control.

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

supports 46+ countries; you own the theming. API onboarding is explicitly not recommended by

Stripe ("resource-intensive… requires regular updates").

that dashboard type is immutable: changing it later means creating a new Account object, so

decide once.

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:

able to hold a worker's money hostage;

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

// 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

// 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;
}
// 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

// 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.

// 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 ifs

// 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