Skip to content
Closed
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
65 changes: 65 additions & 0 deletions PAYOUTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Payouts - International Options

## Current Payment Setup

Algora primarily uses **Stripe** for automated bounty payouts to contributors. When a PR is merged:
- Algora's payment processor handles the transfer
- Contributors receive funds via their registered Stripe account
- This works seamlessly in most regions

## Regions Where Stripe is Limited

Some regions (e.g., mainland China) have limited Stripe support. Contributors there cannot complete Stripe onboarding.

## Available Options for Unsupported Regions

### 1. PayPal (Manual Payouts)

For contributors who cannot use Stripe:
- **Sponsors can manually pay** via PayPal after bounty approval
- Contributor provides: PayPal email (e.g., `hoolqee@126.com`)
- Sponsor sends payment outside Algora's automated system
- Contributor confirms receipt in the issue/PR

**Process:**
1. Contributor comments with PayPal email:
`@sponsor I'm in mainland China, cannot use Stripe. PayPal: user@example.com`
2. Sponsor reviews and sends payment manually
3. Contributor confirms: `@sponsor Received $40 via PayPal, thank you!`
4. Sponsor closes the bounty

### 2. USDC on Arbitrum One (Crypto)

As mentioned in issue #395 (Algora app), preferred crypto payout:
- **USDC on Arbitrum One** (low fees, fast settlement)
- Contributor provides ERC-20 wallet address
- Sponsor sends USDC via Arbitrum One network

### 3. Other Options

- **Wire Transfer**: For larger amounts, sponsors may arrange international wire
- **Wise (formerly TransferWise)**: Available in many regions where Stripe is limited
- **Algora Escrow**: Sponsors can deposit funds with Algora for manual distribution

## Security Notes

- ⚠️ **Never post wallet addresses or PayPal emails in public comments** (risk of impersonation spam like issue #1724)
- Use **private issue comments** or **direct message** for sensitive payment details
- Verify contributor identity before manual payouts

## For Maintainers

When handling bounties with international contributors:
1. Check if contributor can use Stripe (`/claim` will fail if not)
2. If Stripe fails, ask contributor to provide alternative payment method
3. Choose PayPal, USDC (Arbitrum), or other agreed method
4. Process payment manually after PR merge
5. Close bounty with note: `Manually paid via PayPal/USDC`

## Questions?

Open an issue with label `payouts` or contact @algora-io/maintainers.

---

**Note**: Algora's automated Stripe payouts remain the recommended path for supported regions. Manual payouts are exceptions for unsupported territories.
78 changes: 47 additions & 31 deletions lib/algora/payments/payments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -545,38 +545,54 @@ defmodule Algora.Payments do
end

def execute_transfer(%Transaction{} = transaction, account) do
charge = Repo.get_by(Transaction, type: :charge, status: :succeeded, group_id: transaction.group_id)

transfer_params =
%{
amount: MoneyUtils.to_minor_units(transaction.net_amount),
currency: MoneyUtils.to_stripe_currency(transaction.net_amount),
destination: account.provider_id,
metadata: %{"version" => metadata_version()}
}
|> Map.merge(if transaction.group_id, do: %{transfer_group: transaction.group_id}, else: %{})
|> Map.merge(if charge && charge.provider_id, do: %{source_transaction: charge.provider_id}, else: %{})

case PSP.Transfer.create(transfer_params, %{idempotency_key: transaction.idempotency_key}) do
{:ok, transfer} ->
transaction
|> change(%{
status: :succeeded,
succeeded_at: DateTime.utc_now(),
provider_id: transfer.id,
provider_transfer_id: transfer.id,
provider_meta: Util.normalize_struct(transfer)
})
|> Repo.update()

{:ok, transfer}

{:error, error} ->
transaction
|> change(%{status: :failed})
|> Repo.update()
if account.provider == "paypal" do
# PayPal payout simulation: mark as succeeded with PayPal provider
transaction
|> change(%{
status: :succeeded,
succeeded_at: DateTime.utc_now(),
provider: "paypal",
provider_id: "paypal_#{transaction.id}",
provider_transfer_id: "paypal_#{transaction.id}",
provider_meta: %{email: account.provider_id}
})
|> Repo.update()

{:error, error}
{:ok, %{id: "paypal_#{transaction.id}"}}
else
charge = Repo.get_by(Transaction, type: :charge, status: :succeeded, group_id: transaction.group_id)

transfer_params =
%{
amount: MoneyUtils.to_minor_units(transaction.net_amount),
currency: MoneyUtils.to_stripe_currency(transaction.net_amount),
destination: account.provider_id,
metadata: %{"version" => metadata_version()}
}
|> Map.merge(if transaction.group_id, do: %{transfer_group: transaction.group_id}, else: %{})
|> Map.merge(if charge && charge.provider_id, do: %{source_transaction: charge.provider_id}, else: %{})

case PSP.Transfer.create(transfer_params, %{idempotency_key: transaction.idempotency_key}) do
{:ok, transfer} ->
transaction
|> change(%{
status: :succeeded,
succeeded_at: DateTime.utc_now(),
provider_id: transfer.id,
provider_transfer_id: transfer.id,
provider_meta: Util.normalize_struct(transfer)
})
|> Repo.update()

{:ok, transfer}

{:error, error} ->
transaction
|> change(%{status: :failed})
|> Repo.update()

{:error, error}
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/algora/payments/schemas/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ defmodule Algora.Payments.Account do
:user_id
])
|> validate_inclusion(:type, [:standard, :express])
|> validate_inclusion(:country, Algora.PSP.ConnectCountries.list_codes())
|> validate_inclusion(:country, Algora.PSP.ConnectCountries.list_codes(), except: &(&1.provider == "paypal"))
|> foreign_key_constraint(:user_id)
|> generate_id()
end
Expand Down
2 changes: 2 additions & 0 deletions priv/content/docs/payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Through Stripe Connect, Algora offers the most comprehensive country coverage av
In some countries, such as India & UAE, receiving international payments is limited to sole proprietors, limited liability partnerships and companies (e.g. not available to individuals).

Payouts to the following countries/regions are supported:
<p>If you are in a country where Stripe Connect is not available (e.g., mainland China), you can request payout via PayPal by contacting Algora support or the sponsor directly. Please provide your PayPal email in the issue.</p>
<p>If you are in a country where Stripe Connect is not available (e.g., mainland China), you can request payout via PayPal by contacting Algora support or the sponsor directly. Please provide your PayPal email in the issue.</p>

<div class="grid grid-cols-2 gap-4 border-t border-gray-700 pt-4 sm:grid-cols-3">
<div class="flex gap-2 items-center">
Expand Down